From 4adb7764edfc073e2499ccdef178976f52d1bcfc Mon Sep 17 00:00:00 2001 From: Sung Won Cho Date: Thu, 28 Nov 2019 15:59:49 +0800 Subject: [PATCH] Add free plan (#351) * Allow free usage * Update features * Update plan details * Render footer * Type * Unlimited books * Restrict repetition for non pro --- .eslintrc | 2 - package-lock.json | 38 ++--- package.json | 6 +- pkg/server/handlers/routes.go | 28 ++-- pkg/server/operations/users.go | 4 +- pkg/watcher/main.go | 7 +- web/.eslintrc | 71 ---------- web/package-lock.json | 24 +--- web/src/components/Books/index.tsx | 29 ++-- .../components/Common/ItemActions/index.tsx | 5 +- .../SearchBar/AdvancedPanel/BookSearch.tsx | 10 +- .../SearchBar/AdvancedPanel/WordsSearch.tsx | 8 +- .../Header/SearchBar/AdvancedPanel/index.tsx | 9 +- web/src/components/Header/SearchBar/index.tsx | 19 +-- web/src/components/Home/NoteGroup/List.scss | 8 -- web/src/components/Home/NoteGroup/List.tsx | 5 +- web/src/components/Home/index.tsx | 32 ++--- web/src/components/Icons/{Box.js => Box.tsx} | 3 +- web/src/components/New/Content.tsx | 121 ++++++++-------- web/src/components/Repetition/Content.tsx | 34 +++-- web/src/components/Repetition/Edit/index.tsx | 52 +++---- web/src/components/Repetition/New/index.tsx | 37 ++--- .../Repetition/RepetitionItem/Actions.tsx | 5 +- .../Repetition/RepetitionItem/index.tsx | 5 +- .../components/Repetition/RepetitionList.tsx | 5 +- web/src/components/Repetition/index.tsx | 5 +- .../Subscription/Checkout/Form.scss | 6 + .../components/Subscription/Checkout/Form.tsx | 8 +- .../Subscription/Checkout/Sidebar.tsx | 3 +- ...atureItem.module.scss => FeatureItem.scss} | 0 .../{FeatureItem.js => FeatureItem.tsx} | 9 +- ...atureList.module.scss => FeatureList.scss} | 1 - .../{FeatureList.js => FeatureList.tsx} | 16 ++- web/src/components/Subscription/Footer.tsx | 15 ++ .../Subscription/Plan/{Core.js => Core.tsx} | 37 +++-- .../Plan/{Plan.module.scss => Plan.scss} | 43 ++---- .../Subscription/Plan/{Pro.js => Pro.tsx} | 36 ++--- .../components/Subscription/Plan/ProCTA.tsx | 39 +++++ .../Plan/{internal.js => internal.tsx} | 47 +++--- .../components/Subscription/Subscription.scss | 38 ++++- web/src/components/Subscription/index.tsx | 134 +++++++----------- 41 files changed, 494 insertions(+), 510 deletions(-) delete mode 100644 web/.eslintrc rename web/src/components/Icons/{Box.js => Box.tsx} (96%) rename web/src/components/Subscription/{FeatureItem.module.scss => FeatureItem.scss} (100%) rename web/src/components/Subscription/{FeatureItem.js => FeatureItem.tsx} (85%) rename web/src/components/Subscription/{FeatureList.module.scss => FeatureList.scss} (97%) rename web/src/components/Subscription/{FeatureList.js => FeatureList.tsx} (75%) create mode 100644 web/src/components/Subscription/Footer.tsx rename web/src/components/Subscription/Plan/{Core.js => Core.tsx} (59%) rename web/src/components/Subscription/Plan/{Plan.module.scss => Plan.scss} (81%) rename web/src/components/Subscription/Plan/{Pro.js => Pro.tsx} (66%) create mode 100644 web/src/components/Subscription/Plan/ProCTA.tsx rename web/src/components/Subscription/Plan/{internal.js => internal.tsx} (66%) diff --git a/.eslintrc b/.eslintrc index 0ef5a63a..f7b54fc9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -49,7 +49,6 @@ "react", "react-hooks", "import", "prettier", "@typescript-eslint" ], "globals": { - // web "__DEVELOPMENT__": true, "__PRODUCTION__": true, "__DISABLE_SSR__": true, @@ -64,7 +63,6 @@ "webpackIsomorphicTools": true, "StripeCheckout": true, - // browser "browser": true, "chrome": true, __WEB_URL__: true, diff --git a/package-lock.json b/package-lock.json index 531044a1..973843bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -132,12 +132,12 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.8.0.tgz", - "integrity": "sha512-ohqul5s6XEB0AzPWZCuJF5Fd6qC0b4+l5BGEnrlpmvXxvyymb8yw8Bs4YMF8usNAeuCJK87eFIHy8g8GFvOtGA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.9.0.tgz", + "integrity": "sha512-98rfOt3NYn5Gr9wekTB8TexxN6oM8ZRvYuphPs1Atfsy419SDLYCaE30aJkRiiTCwGEY98vOhFsEVm7Zs4toQQ==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "2.8.0", + "@typescript-eslint/experimental-utils": "2.9.0", "eslint-utils": "^1.4.3", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", @@ -145,32 +145,32 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.8.0.tgz", - "integrity": "sha512-jZ05E4SxCbbXseQGXOKf3ESKcsGxT8Ucpkp1jiVp55MGhOvZB2twmWKf894PAuVQTCgbPbJz9ZbRDqtUWzP8xA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.9.0.tgz", + "integrity": "sha512-0lOLFdpdJsCMqMSZT7l7W2ta0+GX8A3iefG3FovJjrX+QR8y6htFlFdU7aOVPL6pDvt6XcsOb8fxk5sq+girTw==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.8.0", + "@typescript-eslint/typescript-estree": "2.9.0", "eslint-scope": "^5.0.0" } }, "@typescript-eslint/parser": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.8.0.tgz", - "integrity": "sha512-NseXWzhkucq+JM2HgqAAoKEzGQMb5LuTRjFPLQzGIdLthXMNUfuiskbl7QSykvWW6mvzCtYbw1fYWGa2EIaekw==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.9.0.tgz", + "integrity": "sha512-fJ+dNs3CCvEsJK2/Vg5c2ZjuQ860ySOAsodDPwBaVlrGvRN+iCNC8kUfLFL8cT49W4GSiLPa/bHiMjYXA7EhKQ==", "dev": true, "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.8.0", - "@typescript-eslint/typescript-estree": "2.8.0", + "@typescript-eslint/experimental-utils": "2.9.0", + "@typescript-eslint/typescript-estree": "2.9.0", "eslint-visitor-keys": "^1.1.0" } }, "@typescript-eslint/typescript-estree": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.8.0.tgz", - "integrity": "sha512-ksvjBDTdbAQ04cR5JyFSDX113k66FxH1tAXmi+dj6hufsl/G0eMc/f1GgLjEVPkYClDbRKv+rnBFuE5EusomUw==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.9.0.tgz", + "integrity": "sha512-v6btSPXEWCP594eZbM+JCXuFoXWXyF/z8kaSBSdCb83DF+Y7+xItW29SsKtSULgLemqJBT+LpT+0ZqdfH7QVmA==", "dev": true, "requires": { "debug": "^4.1.1", @@ -498,9 +498,9 @@ "dev": true }, "eslint": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.7.0.tgz", - "integrity": "sha512-dQpj+PaHKHfXHQ2Imcw5d853PTvkUGbHk/MR68KQUl98EgKDCdh4vLRH1ZxhqeQjQFJeg8fgN0UwmNhN3l8dDQ==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.7.1.tgz", + "integrity": "sha512-UWzBS79pNcsDSxgxbdjkmzn/B6BhsXMfUaOHnNwyE8nD+Q6pyT96ow2MccVayUTV4yMid4qLhMiQaywctRkBLA==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", diff --git a/package.json b/package.json index f84985e1..192de744 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,10 @@ "description": "Dnote monorepo", "license": "SEE LICENSE IN LICENSE", "devDependencies": { - "@typescript-eslint/eslint-plugin": "^2.8.0", - "@typescript-eslint/parser": "^2.8.0", + "@typescript-eslint/eslint-plugin": "^2.9.0", + "@typescript-eslint/parser": "^2.9.0", "babel-eslint": "^10.0.3", - "eslint": "^6.7.0", + "eslint": "^6.7.1", "eslint-config-airbnb": "^18.0.1", "eslint-config-prettier": "^6.7.0", "eslint-plugin-import": "^2.18.2", diff --git a/pkg/server/handlers/routes.go b/pkg/server/handlers/routes.go index 7efb1b95..3dc19890 100644 --- a/pkg/server/handlers/routes.go +++ b/pkg/server/handlers/routes.go @@ -200,6 +200,7 @@ func (a *App) auth(next http.HandlerFunc, p *AuthMiddlewareParams) http.HandlerF if p != nil && p.ProOnly { if !user.Cloud { respondForbidden(w) + return } } @@ -237,6 +238,7 @@ func (a *App) tokenAuth(next http.HandlerFunc, tokenType string, p *AuthMiddlewa if p != nil && p.ProOnly { if !user.Cloud { respondForbidden(w) + return } } @@ -360,10 +362,10 @@ func NewRouter(app *App) (*mux.Router, error) { {"GET", "/subscriptions", app.auth(app.getSub, nil), true}, {"GET", "/stripe_source", app.auth(app.getStripeSource, nil), true}, {"PATCH", "/stripe_source", app.auth(app.updateStripeSource, nil), true}, - {"GET", "/notes", app.auth(app.getNotes, &proOnly), false}, + {"GET", "/notes", app.auth(app.getNotes, nil), false}, {"GET", "/notes/{noteUUID}", app.getNote, true}, - {"GET", "/calendar", app.auth(app.getCalendar, &proOnly), true}, - {"GET", "/repetition_rules", app.auth(app.getRepetitionRules, &proOnly), true}, + {"GET", "/calendar", app.auth(app.getCalendar, nil), true}, + {"GET", "/repetition_rules", app.auth(app.getRepetitionRules, nil), true}, {"GET", "/repetition_rules/{repetitionRuleUUID}", app.tokenAuth(app.getRepetitionRule, database.TokenTypeRepetition, &proOnly), true}, {"POST", "/repetition_rules", app.auth(app.createRepetitionRule, &proOnly), true}, {"PATCH", "/repetition_rules/{repetitionRuleUUID}", app.tokenAuth(app.updateRepetitionRule, database.TokenTypeRepetition, &proOnly), true}, @@ -377,18 +379,18 @@ func NewRouter(app *App) (*mux.Router, error) { {"PATCH", "/classic/set-password", app.auth(app.classicSetPassword, nil), true}, // v3 - {"GET", "/v3/sync/fragment", cors(app.auth(app.GetSyncFragment, &proOnly)), false}, - {"GET", "/v3/sync/state", cors(app.auth(app.GetSyncState, &proOnly)), false}, + {"GET", "/v3/sync/fragment", cors(app.auth(app.GetSyncFragment, nil)), false}, + {"GET", "/v3/sync/state", cors(app.auth(app.GetSyncState, nil)), false}, {"OPTIONS", "/v3/books", cors(app.BooksOptions), true}, - {"GET", "/v3/books", cors(app.auth(app.GetBooks, &proOnly)), true}, - {"GET", "/v3/books/{bookUUID}", cors(app.auth(app.GetBook, &proOnly)), true}, - {"POST", "/v3/books", cors(app.auth(app.CreateBook, &proOnly)), false}, - {"PATCH", "/v3/books/{bookUUID}", cors(app.auth(app.UpdateBook, &proOnly)), false}, - {"DELETE", "/v3/books/{bookUUID}", cors(app.auth(app.DeleteBook, &proOnly)), false}, + {"GET", "/v3/books", cors(app.auth(app.GetBooks, nil)), true}, + {"GET", "/v3/books/{bookUUID}", cors(app.auth(app.GetBook, nil)), true}, + {"POST", "/v3/books", cors(app.auth(app.CreateBook, nil)), false}, + {"PATCH", "/v3/books/{bookUUID}", cors(app.auth(app.UpdateBook, nil)), false}, + {"DELETE", "/v3/books/{bookUUID}", cors(app.auth(app.DeleteBook, nil)), false}, {"OPTIONS", "/v3/notes", cors(app.NotesOptions), true}, - {"POST", "/v3/notes", cors(app.auth(app.CreateNote, &proOnly)), false}, - {"PATCH", "/v3/notes/{noteUUID}", app.auth(app.UpdateNote, &proOnly), false}, - {"DELETE", "/v3/notes/{noteUUID}", app.auth(app.DeleteNote, &proOnly), false}, + {"POST", "/v3/notes", cors(app.auth(app.CreateNote, nil)), false}, + {"PATCH", "/v3/notes/{noteUUID}", app.auth(app.UpdateNote, nil), false}, + {"DELETE", "/v3/notes/{noteUUID}", app.auth(app.DeleteNote, nil), false}, {"POST", "/v3/signin", cors(app.signin), true}, {"OPTIONS", "/v3/signout", cors(app.signoutOptions), true}, {"POST", "/v3/signout", cors(app.signout), true}, diff --git a/pkg/server/operations/users.go b/pkg/server/operations/users.go index c770b3ce..b616d524 100644 --- a/pkg/server/operations/users.go +++ b/pkg/server/operations/users.go @@ -86,9 +86,9 @@ func createEmailPreference(user database.User, tx *gorm.DB) error { func createDefaultRepetitionRule(user database.User, tx *gorm.DB) error { r := database.RepetitionRule{ - Title: "Default repetition - all bookx", + Title: "Default repetition - all book", UserID: user.ID, - Enabled: true, + Enabled: false, Hour: 20, Minute: 30, Frequency: 604800000, diff --git a/pkg/watcher/main.go b/pkg/watcher/main.go index c7fc5e9a..426b61da 100644 --- a/pkg/watcher/main.go +++ b/pkg/watcher/main.go @@ -106,8 +106,11 @@ func main() { }() if ignore != "" { - if err := w.Ignore(ignore); err != nil { - log.Fatalln(errors.Wrapf(err, "ignoring %s", ignore)) + files := strings.Split(ignore, ",") + for _, file := range files { + if err := w.Ignore(file); err != nil { + log.Fatalln(errors.Wrapf(err, "ignoring %s", file)) + } } } diff --git a/web/.eslintrc b/web/.eslintrc deleted file mode 100644 index d46d8628..00000000 --- a/web/.eslintrc +++ /dev/null @@ -1,71 +0,0 @@ -{ "extends": ["eslint-config-airbnb", "prettier"], - "env": { - "browser": true, - "node": true, - "jest": true - }, - "parser": "@typescript-eslint/parser", - "rules": { - "camelcase": 0, - "strict": 0, - "react/no-multi-comp": 0, - "import/default": 0, - "import/no-duplicates": 0, - "import/named": 0, - "import/namespace": 0, - "import/no-unresolved": 0, - "import/no-named-as-default": 2, - "import/prefer-default-export": 0, - "comma-dangle": 0, // not sure why airbnb turned this on. gross! - "indent": [2, 2, {"SwitchCase": 1}], - "no-console": 0, - "no-alert": 0, - "arrow-body-style": 0, - "react/prop-types": 0, - "react/jsx-filename-extension": 0, - "react/prefer-stateless-function": 0, - "jsx-a11y/anchor-is-valid": 0, - "jsx-a11y/tabindex-no-positive": 0, - "no-mixed-operators": 0, - "no-plusplus": 0, - "no-underscore-dangle": 0, - "prettier/prettier": "error", - "jsx-a11y/no-autofocus": 0, - "jsx-a11y/label-has-for": 0, - "prefer-destructuring": 0, - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", - "react/jsx-wrap-multilines": ["error", {"declaration": false, "assignment": false}], - "react/jsx-one-expression-per-line": 0, - "@typescript-eslint/no-unused-vars": 1, - "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*_test.ts", "**/webpack/**/*"]}], - "lines-between-class-members": 0, - "react/jsx-fragments": 0, - "jsx-a11y/label-has-associated-control": 0 - }, - "plugins": [ - "react", "react-hooks", "import", "prettier", "@typescript-eslint" - ], - "settings": { - "import/parser": "babel-eslint", - "import/resolve": { - "moduleDirectory": ["node_modules", "src"] - } - }, - "globals": { - "__DEVELOPMENT__": true, - "__PRODUCTION__": true, - "__DISABLE_SSR__": true, - "__DEVTOOLS__": true, - "__DOMAIN__": true, - "__BASE_URL__": true, - "__BASE_NAME__": true, - "__STRIPE_PUBLIC_KEY__": true, - "__ROOT_URL__": true, - "__CDN_URL__": true, - "__VERSION__": true, - "socket": true, - "webpackIsomorphicTools": true, - "StripeCheckout": true - } -} diff --git a/web/package-lock.json b/web/package-lock.json index 5dad320f..96b12c0a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -7722,14 +7722,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7744,20 +7742,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -7874,8 +7869,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -7887,7 +7881,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7902,7 +7895,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7916,7 +7908,6 @@ "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -8015,8 +8006,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -8028,7 +8018,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -8150,7 +8139,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/web/src/components/Books/index.tsx b/web/src/components/Books/index.tsx index 229121ee..9fa8b126 100644 --- a/web/src/components/Books/index.tsx +++ b/web/src/components/Books/index.tsx @@ -20,7 +20,6 @@ import React, { useEffect, useState, Fragment } from 'react'; import { getBooks } from '../../store/books'; import { useDispatch } from '../../store'; -import PayWall from '../Common/PayWall'; import Content from './Content'; import Flash from '../Common/Flash'; import HeadData from './HeadData'; @@ -37,22 +36,20 @@ const Books: React.FunctionComponent = () => { - -
-
- { - setSuccessMessage(''); - }} - > - {successMessage} - -
- ; +
+
+ { + setSuccessMessage(''); + }} + > + {successMessage} +
- + ; +
); }; diff --git a/web/src/components/Common/ItemActions/index.tsx b/web/src/components/Common/ItemActions/index.tsx index e8c3f984..c6f263a3 100644 --- a/web/src/components/Common/ItemActions/index.tsx +++ b/web/src/components/Common/ItemActions/index.tsx @@ -31,6 +31,7 @@ interface Props { isOpen: boolean; setIsOpen: React.Dispatch; wrapperClassName?: string; + disabled?: boolean; } const ItemActions: React.FunctionComponent = ({ @@ -41,7 +42,8 @@ const ItemActions: React.FunctionComponent = ({ optRefs, isOpen, setIsOpen, - wrapperClassName + wrapperClassName, + disabled }) => { return (
= ({ contentClassName={styles.content} alignment="right" direction="bottom" + disabled={disabled} />
); diff --git a/web/src/components/Header/SearchBar/AdvancedPanel/BookSearch.tsx b/web/src/components/Header/SearchBar/AdvancedPanel/BookSearch.tsx index d319c678..ee46afb2 100644 --- a/web/src/components/Header/SearchBar/AdvancedPanel/BookSearch.tsx +++ b/web/src/components/Header/SearchBar/AdvancedPanel/BookSearch.tsx @@ -29,7 +29,6 @@ import styles from './AdvancedPanel.scss'; interface Props { value: string; setValue: (string) => void; - disabled: boolean; } // getCurrentTerm returns the current term in the comma separated @@ -113,11 +112,7 @@ function useSetSuggestionVisibility( }, [setIsOpen, triggerRef, inputValue, prevInputValue]); } -const BookSearch: React.FunctionComponent = ({ - value, - setValue, - disabled -}) => { +const BookSearch: React.FunctionComponent = ({ value, setValue }) => { const [isOpen, setIsOpen] = useState(false); const [focusedIdx, setFocusedIdx] = useState(0); const [focusedOptEl, setFocusedOptEl] = useState(null); @@ -141,7 +136,7 @@ const BookSearch: React.FunctionComponent = ({ focusedIdx, setFocusedIdx, onKeydownSelect: appendBook, - disabled: !isOpen || disabled + disabled: !isOpen }); useScrollToFocused({ shouldScroll: true, @@ -167,7 +162,6 @@ const BookSearch: React.FunctionComponent = ({ styles.input )} value={value} - disabled={disabled} onChange={e => { const val = e.target.value; diff --git a/web/src/components/Header/SearchBar/AdvancedPanel/WordsSearch.tsx b/web/src/components/Header/SearchBar/AdvancedPanel/WordsSearch.tsx index 50afb13b..65277056 100644 --- a/web/src/components/Header/SearchBar/AdvancedPanel/WordsSearch.tsx +++ b/web/src/components/Header/SearchBar/AdvancedPanel/WordsSearch.tsx @@ -24,14 +24,9 @@ import styles from './AdvancedPanel.scss'; interface Props { words: string; setWords: (string) => void; - disabled: boolean; } -const WordsSearch: React.FunctionComponent = ({ - words, - setWords, - disabled -}) => { +const WordsSearch: React.FunctionComponent = ({ words, setWords }) => { return (
); }; diff --git a/web/src/components/Subscription/Checkout/Sidebar.tsx b/web/src/components/Subscription/Checkout/Sidebar.tsx index afdea9d0..90906896 100644 --- a/web/src/components/Subscription/Checkout/Sidebar.tsx +++ b/web/src/components/Subscription/Checkout/Sidebar.tsx @@ -29,7 +29,8 @@ const perks = [ { id: 'hosted', icon: , - value: 'Fully hosted and managed' + value: + 'Maximize your memory retention and become the best learner you can be' }, { id: 'support', diff --git a/web/src/components/Subscription/FeatureItem.module.scss b/web/src/components/Subscription/FeatureItem.scss similarity index 100% rename from web/src/components/Subscription/FeatureItem.module.scss rename to web/src/components/Subscription/FeatureItem.scss diff --git a/web/src/components/Subscription/FeatureItem.js b/web/src/components/Subscription/FeatureItem.tsx similarity index 85% rename from web/src/components/Subscription/FeatureItem.js rename to web/src/components/Subscription/FeatureItem.tsx index 889e7f21..2f793779 100644 --- a/web/src/components/Subscription/FeatureItem.js +++ b/web/src/components/Subscription/FeatureItem.tsx @@ -19,10 +19,13 @@ import React from 'react'; import CheckIcon from '../Icons/Check'; +import styles from './FeatureItem.scss'; -import styles from './FeatureItem.module.scss'; +interface Props { + label: string; +} -export default ({ label }) => { +const FeatureItem: React.FunctionComponent = ({ label }) => { return (
  • @@ -31,3 +34,5 @@ export default ({ label }) => {
  • ); }; + +export default FeatureItem; diff --git a/web/src/components/Subscription/FeatureList.module.scss b/web/src/components/Subscription/FeatureList.scss similarity index 97% rename from web/src/components/Subscription/FeatureList.module.scss rename to web/src/components/Subscription/FeatureList.scss index bbe964b9..b0528200 100644 --- a/web/src/components/Subscription/FeatureList.module.scss +++ b/web/src/components/Subscription/FeatureList.scss @@ -25,5 +25,4 @@ list-style: none; margin-bottom: 0; padding-left: 0; - padding-top: rem(12px); } diff --git a/web/src/components/Subscription/FeatureList.js b/web/src/components/Subscription/FeatureList.tsx similarity index 75% rename from web/src/components/Subscription/FeatureList.js rename to web/src/components/Subscription/FeatureList.tsx index 8380f1ca..bb0ace6f 100644 --- a/web/src/components/Subscription/FeatureList.js +++ b/web/src/components/Subscription/FeatureList.tsx @@ -17,19 +17,27 @@ */ import React from 'react'; +import classnames from 'classnames'; import FeatureItem from './FeatureItem'; +import styles from './FeatureList.scss'; -import styles from './FeatureList.module.scss'; +interface Props { + features: any; + wrapperClassName?: string; +} -function FeatureList({ features }) { +const FeatureList: React.FunctionComponent = ({ + features, + wrapperClassName +}) => { return ( -
      +
        {features.map(feature => { return ; })}
      ); -} +}; export default FeatureList; diff --git a/web/src/components/Subscription/Footer.tsx b/web/src/components/Subscription/Footer.tsx new file mode 100644 index 00000000..8e884b4e --- /dev/null +++ b/web/src/components/Subscription/Footer.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import styles from './Subscription.scss'; + +interface Props {} + +const Footer: React.FunctionComponent = () => { + return ( +
      + © 2019 Monomax Software Pty Ltd +
      + ); +}; + +export default Footer; diff --git a/web/src/components/Subscription/Plan/Core.js b/web/src/components/Subscription/Plan/Core.tsx similarity index 59% rename from web/src/components/Subscription/Plan/Core.js rename to web/src/components/Subscription/Plan/Core.tsx index 92b0047c..97400d68 100644 --- a/web/src/components/Subscription/Plan/Core.js +++ b/web/src/components/Subscription/Plan/Core.tsx @@ -18,28 +18,41 @@ import React from 'react'; -import BoxIcon from '../../Icons/Box'; +import { UserData } from 'jslib/operations/types'; import Plan from './internal'; -const selfHostedPerks = [ - { - id: 'own-machine', - icon: , - value: 'Host on your own machine' - } -]; +const desc = + 'Streamline your learnings into your own personal knowledge base. You can access any item at any time.'; -function Core({ wrapperClassName, ctaContent, bottomContent }) { +interface Props { + wrapperClassName: string; + user: UserData; + bottomContent: React.ReactElement; +} + +const Core: React.FunctionComponent = ({ + wrapperClassName, + user, + bottomContent +}) => { return ( + {user && user.pro ? 'Already upgraded!' : 'Your current plan'} + + } bottomContent={bottomContent} /> ); -} +}; export default Core; diff --git a/web/src/components/Subscription/Plan/Plan.module.scss b/web/src/components/Subscription/Plan/Plan.scss similarity index 81% rename from web/src/components/Subscription/Plan/Plan.module.scss rename to web/src/components/Subscription/Plan/Plan.scss index 7f81b717..8970191d 100644 --- a/web/src/components/Subscription/Plan/Plan.module.scss +++ b/web/src/components/Subscription/Plan/Plan.scss @@ -54,7 +54,7 @@ } } .header-body { - margin-top: rem(20px); + margin-top: rem(32px); } .name { @@ -65,20 +65,23 @@ } .price-wrapper { - display: flex; - align-items: baseline; + @include font-size('large'); + font-weight: 600; + margin-top: rem(12px); } .price { - @include font-size('3x-large'); + display: inline-block; font-weight: 600; } .interval { - @include font-size('large'); - text-transform: uppercase; - font-weight: 600; - margin-left: rem(4px); + display: inline-block; +} + +.desc { + margin-top: rem(20px); + @include font-size('regular'); } .cta-wrapper { @@ -92,30 +95,6 @@ padding-top: rem(12px); } -.perks { - margin-top: rem(12px); -} -.perk-value { - @include font-size('regular'); - margin-left: rem(8px); -} -.perk-item { - display: flex; - flex-shrink: 1; - - &:not(:first-child) { - margin-top: rem(4px); - } -} - -.perk-icon { - flex-shrink: 0; - display: flex; - justify-content: center; - align-items: flex-start; - margin-top: rem(4px); -} - .feature-bold { font-weight: 600; } diff --git a/web/src/components/Subscription/Plan/Pro.js b/web/src/components/Subscription/Plan/Pro.tsx similarity index 66% rename from web/src/components/Subscription/Plan/Pro.js rename to web/src/components/Subscription/Plan/Pro.tsx index 26638191..00bf2e79 100644 --- a/web/src/components/Subscription/Plan/Pro.js +++ b/web/src/components/Subscription/Plan/Pro.tsx @@ -18,35 +18,35 @@ import React from 'react'; +import { UserData } from 'jslib/operations/types'; import Plan from './internal'; -import ServerIcon from '../../Icons/Server'; -import GlobeIcon from '../../Icons/Globe'; +import ProCTA from './ProCTA'; -const proPerks = [ - { - id: 'hosted', - icon: , - value: 'Fully hosted and managed' - }, - { - id: 'support', - icon: , - value: 'Support the Dnote community and development' - } -]; +const desc = + 'Maximize your memory retention and become the best learner you can be.'; -function ProPlan({ wrapperClassName, ctaContent, bottomContent }) { +interface Props { + wrapperClassName: string; + user: UserData; + bottomContent: React.ReactElement; +} + +const ProPlan: React.FunctionComponent = ({ + wrapperClassName, + user, + bottomContent +}) => { return ( } bottomContent={bottomContent} /> ); -} +}; export default ProPlan; diff --git a/web/src/components/Subscription/Plan/ProCTA.tsx b/web/src/components/Subscription/Plan/ProCTA.tsx new file mode 100644 index 00000000..aaa4708c --- /dev/null +++ b/web/src/components/Subscription/Plan/ProCTA.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import classnames from 'classnames'; + +import { + getSubscriptionCheckoutPath, + getSettingsPath, + SettingSections +} from 'web/libs/paths'; +import { UserData } from 'jslib/operations/types'; + +interface Props { + user: UserData; +} + +const ProCTA: React.FunctionComponent = ({ user }) => { + if (user && user.pro) { + return ( + + Manage Your Plan + + ); + } + + return ( + + Upgrade + + ); +}; + +export default ProCTA; diff --git a/web/src/components/Subscription/Plan/internal.js b/web/src/components/Subscription/Plan/internal.tsx similarity index 66% rename from web/src/components/Subscription/Plan/internal.js rename to web/src/components/Subscription/Plan/internal.tsx index 682375fe..eed4c50a 100644 --- a/web/src/components/Subscription/Plan/internal.js +++ b/web/src/components/Subscription/Plan/internal.tsx @@ -19,17 +19,33 @@ import React from 'react'; import classnames from 'classnames'; -import styles from './Plan.module.scss'; +import styles from './Plan.scss'; -function Plan({ +interface Perk { + id: string; + icon: JSX.Element; + value: string; +} + +interface Props { + name: string; + desc: string; + price: string; + bottomContent: React.ReactElement; + ctaContent: React.ReactElement; + interval?: string; + wrapperClassName: string; +} + +const Plan: React.FunctionComponent = ({ name, + desc, price, bottomContent, ctaContent, interval, - perks, wrapperClassName -}) { +}) => { return (

      {name}

      -
        - {perks.map(perk => { - return ( -
      • -
        {perk.icon}
        -
        {perk.value}
        -
      • - ); - })} -
      +
      + {price}{' '} + {interval &&
      / {interval}
      } +
      + +

      {desc}

      -
      - {price} - {interval &&
      / {interval}
      } -
      -
      {ctaContent}
      @@ -63,6 +70,6 @@ function Plan({ {bottomContent}
      ); -} +}; export default Plan; diff --git a/web/src/components/Subscription/Subscription.scss b/web/src/components/Subscription/Subscription.scss index 2660a824..7904cf75 100644 --- a/web/src/components/Subscription/Subscription.scss +++ b/web/src/components/Subscription/Subscription.scss @@ -18,12 +18,34 @@ @import '../App/responsive'; @import '../App/font'; +@import '../App/theme'; @import '../App/rem'; .wrapper { - // padding-bottom: rem(80px); background: white; - height: calc(100vh - #{$header-height}); + display: flex; + flex-direction: column; + justify-content: center; + + @include breakpoint(md) { + height: calc(100vh - #{$header-height}); + } +} + +.content { + margin-bottom: rem(40px); + + @include breakpoint(md) { + margin-bottom: auto; + } +} + +.footer { + margin-top: auto; + padding: rem(12px) 0; + text-align: center; + @include font-size('x-small'); + color: $gray; } .hero { @@ -94,3 +116,15 @@ .feature-bold { font-weight: 600; } + +.bottom { + padding-top: rem(20px); +} + +.pro-prelude { + padding: 0 rem(8px); +} + +.pro-feature-list { + margin-top: rem(12px); +} diff --git a/web/src/components/Subscription/index.tsx b/web/src/components/Subscription/index.tsx index 5c029cfa..00daa0db 100644 --- a/web/src/components/Subscription/index.tsx +++ b/web/src/components/Subscription/index.tsx @@ -18,29 +18,19 @@ import React from 'react'; import Helmet from 'react-helmet'; -import { Link } from 'react-router-dom'; -import classnames from 'classnames'; -import { getSubscriptionCheckoutPath } from 'web/libs/paths'; import ProPlan from './Plan/Pro'; import CorePlan from './Plan/Core'; import FeatureList from './FeatureList'; +import Footer from './Footer'; import { useSelector } from '../../store'; import styles from './Subscription.scss'; const proFeatures = [ { - id: 'core', - label:
      Everything in core
      - }, - { - id: 'host', - label:
      Hosting
      - }, - { - id: 'auto', - label:
      Automatic update and migration
      + id: 'spaced-rep', + label:
      Automated Spaced Repetition
      }, { id: 'email-support', @@ -48,7 +38,19 @@ const proFeatures = [ } ]; -const baseFeatures = [ +const coreFeatures = [ + { + id: 'oss', + label:
      Open source
      + }, + { + id: 'num-notes', + label:
      Unlimited notes
      + }, + { + id: 'num-books', + label:
      Unlimited books
      + }, { id: 'sync', label:
      Multi-device sync
      @@ -57,25 +59,13 @@ const baseFeatures = [ id: 'cli', label:
      Command line interface
      }, - { - id: 'atom', - label:
      Atom plugin
      - }, { id: 'web', - label:
      Web client
      - }, - { - id: 'digest', - label:
      Automated email digest
      + label:
      Web application
      }, { id: 'ext', - label:
      Firefox/Chrome extension
      - }, - { - id: 'foss', - label:
      Free and open source
      + label:
      Chrome/Firefox extension
      }, { id: 'forum-support', @@ -92,31 +82,6 @@ const Subscription: React.FunctionComponent = () => { }; }); - function renderPlanCta() { - if (user && user.pro) { - return ( - - Go to your notes - - ); - } - - return ( - - Unlock - - ); - } - return (
      @@ -127,38 +92,45 @@ const Subscription: React.FunctionComponent = () => { /> -
      +
      +
      +
      +

      Choose your Dnote plan.

      +
      +
      +
      -

      - You can self-host or sign up for the hosted version. -

      +
      + + +
      + } + /> + + +
      + Everything from the core plan, plus: +
      + +
      + } + /> +
      -
      -
      - - See source code - - } - bottomContent={} - /> - - } - /> -
      -
      +