mirror of
https://github.com/dnote/dnote
synced 2026-03-14 22:45:50 +01:00
Add free plan (#351)
* Allow free usage * Update features * Update plan details * Render footer * Type * Unlimited books * Restrict repetition for non pro
This commit is contained in:
parent
0e83ba1a5c
commit
4adb7764ed
41 changed files with 492 additions and 508 deletions
|
|
@ -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,
|
||||
|
|
|
|||
38
package-lock.json
generated
38
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
24
web/package-lock.json
generated
24
web/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
<Fragment>
|
||||
<HeadData />
|
||||
|
||||
<PayWall>
|
||||
<div className="page page-mobile-full">
|
||||
<div className="container mobile-nopadding">
|
||||
<Flash
|
||||
kind="success"
|
||||
when={Boolean(successMessage)}
|
||||
onDismiss={() => {
|
||||
setSuccessMessage('');
|
||||
}}
|
||||
>
|
||||
{successMessage}
|
||||
</Flash>
|
||||
</div>
|
||||
<Content setSuccessMessage={setSuccessMessage} />;
|
||||
<div className="page page-mobile-full">
|
||||
<div className="container mobile-nopadding">
|
||||
<Flash
|
||||
kind="success"
|
||||
when={Boolean(successMessage)}
|
||||
onDismiss={() => {
|
||||
setSuccessMessage('');
|
||||
}}
|
||||
>
|
||||
{successMessage}
|
||||
</Flash>
|
||||
</div>
|
||||
</PayWall>
|
||||
<Content setSuccessMessage={setSuccessMessage} />;
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ interface Props {
|
|||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<any>;
|
||||
wrapperClassName?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ItemActions: React.FunctionComponent<Props> = ({
|
||||
|
|
@ -41,7 +42,8 @@ const ItemActions: React.FunctionComponent<Props> = ({
|
|||
optRefs,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
wrapperClassName
|
||||
wrapperClassName,
|
||||
disabled
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -62,6 +64,7 @@ const ItemActions: React.FunctionComponent<Props> = ({
|
|||
contentClassName={styles.content}
|
||||
alignment="right"
|
||||
direction="bottom"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<Props> = ({
|
||||
value,
|
||||
setValue,
|
||||
disabled
|
||||
}) => {
|
||||
const BookSearch: React.FunctionComponent<Props> = ({ 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<Props> = ({
|
|||
focusedIdx,
|
||||
setFocusedIdx,
|
||||
onKeydownSelect: appendBook,
|
||||
disabled: !isOpen || disabled
|
||||
disabled: !isOpen
|
||||
});
|
||||
useScrollToFocused({
|
||||
shouldScroll: true,
|
||||
|
|
@ -167,7 +162,6 @@ const BookSearch: React.FunctionComponent<Props> = ({
|
|||
styles.input
|
||||
)}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={e => {
|
||||
const val = e.target.value;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,14 +24,9 @@ import styles from './AdvancedPanel.scss';
|
|||
interface Props {
|
||||
words: string;
|
||||
setWords: (string) => void;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const WordsSearch: React.FunctionComponent<Props> = ({
|
||||
words,
|
||||
setWords,
|
||||
disabled
|
||||
}) => {
|
||||
const WordsSearch: React.FunctionComponent<Props> = ({ words, setWords }) => {
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<label htmlFor="has-words" className={styles.label}>
|
||||
|
|
@ -44,7 +39,6 @@ const WordsSearch: React.FunctionComponent<Props> = ({
|
|||
styles.input
|
||||
)}
|
||||
value={words}
|
||||
disabled={disabled}
|
||||
onChange={e => {
|
||||
const val = e.target.value;
|
||||
setWords(val);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import styles from './AdvancedPanel.scss';
|
|||
|
||||
interface Props extends RouteComponentProps {
|
||||
onDismiss: () => void;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
// quoteFilters surrounds a filter term with a pair of double quotation marks, effectively
|
||||
|
|
@ -107,8 +106,7 @@ function encodeBookStr(s: string): string[] {
|
|||
const AdvancedPanel: React.FunctionComponent<Props> = ({
|
||||
onDismiss,
|
||||
history,
|
||||
location,
|
||||
disabled
|
||||
location
|
||||
}) => {
|
||||
const filters = useFilters();
|
||||
const { queries } = filters;
|
||||
|
|
@ -139,9 +137,9 @@ const AdvancedPanel: React.FunctionComponent<Props> = ({
|
|||
closeOnOutsideClick
|
||||
>
|
||||
<form className={styles.form} onSubmit={handleSubmit}>
|
||||
<WordsSearch words={words} setWords={setWords} disabled={disabled} />
|
||||
<WordsSearch words={words} setWords={setWords} />
|
||||
|
||||
<BookSearch value={books} setValue={setBooks} disabled={disabled} />
|
||||
<BookSearch value={books} setValue={setBooks} />
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
|
|
@ -149,7 +147,6 @@ const AdvancedPanel: React.FunctionComponent<Props> = ({
|
|||
size="normal"
|
||||
stretch
|
||||
className={styles.submit}
|
||||
disabled={disabled}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import * as filtersLib from 'jslib/helpers/filters';
|
|||
import * as queriesLib from 'jslib/helpers/queries';
|
||||
import { getSearchDest } from 'web/libs/search';
|
||||
import { usePrevious } from 'web/libs/hooks';
|
||||
import { useFilters, useSelector } from '../../../store';
|
||||
import { useFilters } from '../../../store';
|
||||
import SearchInput from '../../Common/SearchInput';
|
||||
import AdvancedPanel from './AdvancedPanel';
|
||||
import styles from './SearchBar.scss';
|
||||
|
|
@ -41,23 +41,13 @@ const SearchBar: React.FunctionComponent<Props> = ({ location, history }) => {
|
|||
const [value, setValue] = useState(initialValue);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const { user } = useSelector(state => {
|
||||
return {
|
||||
user: state.auth.user.data
|
||||
};
|
||||
});
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(queryText: string) => {
|
||||
if (!user.pro) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queries = queriesLib.parse(queryText);
|
||||
const dest = getSearchDest(location, queries);
|
||||
history.push(dest);
|
||||
},
|
||||
[history, location, user]
|
||||
[history, location]
|
||||
);
|
||||
|
||||
const prevFilters = usePrevious(filters);
|
||||
|
|
@ -76,8 +66,6 @@ const SearchBar: React.FunctionComponent<Props> = ({ location, history }) => {
|
|||
setExpanded(false);
|
||||
};
|
||||
|
||||
const disabled = !user.pro;
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<form
|
||||
|
|
@ -93,7 +81,6 @@ const SearchBar: React.FunctionComponent<Props> = ({ location, history }) => {
|
|||
wrapperClassName={styles['input-wrapper']}
|
||||
inputClassName={classnames(styles.input, ' text-input-small')}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={e => {
|
||||
const val = e.target.value;
|
||||
setValue(val);
|
||||
|
|
@ -120,7 +107,7 @@ const SearchBar: React.FunctionComponent<Props> = ({ location, history }) => {
|
|||
</button>
|
||||
</form>
|
||||
|
||||
{expanded && <AdvancedPanel onDismiss={onDismiss} disabled={disabled} />}
|
||||
{expanded && <AdvancedPanel onDismiss={onDismiss} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,14 +24,6 @@
|
|||
.wrapper {
|
||||
flex-grow: 1;
|
||||
|
||||
&.nopro {
|
||||
display: none;
|
||||
|
||||
@include breakpoint(lg) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(lg) {
|
||||
margin-top: rem(16px);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { Filters } from 'jslib/helpers/filters';
|
||||
import { NotesGroupData } from 'web/libs/notes';
|
||||
|
|
@ -59,18 +58,16 @@ function renderResult({
|
|||
interface Props {
|
||||
isFetched: boolean;
|
||||
groups: NotesGroupData[];
|
||||
pro: boolean;
|
||||
filters: Filters;
|
||||
}
|
||||
|
||||
const NoteGroupList: React.FunctionComponent<Props> = ({
|
||||
groups,
|
||||
pro,
|
||||
filters,
|
||||
isFetched
|
||||
}) => {
|
||||
return (
|
||||
<div className={classnames(styles.wrapper, { [styles.nopro]: !pro })}>
|
||||
<div className={styles.wrapper}>
|
||||
{renderResult({ groups, isFetched, filters })}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import { useDispatch, useSelector } from '../../store';
|
|||
import { getNotes } from '../../store/notes';
|
||||
import TopActions from './Actions/Top';
|
||||
import Flash from '../Common/Flash';
|
||||
import PayWall from '../Common/PayWall';
|
||||
|
||||
interface Props extends RouteComponentProps {}
|
||||
|
||||
|
|
@ -47,9 +46,6 @@ function useFetchNotes(filters: Filters) {
|
|||
const prevFilters = usePrevious(filters);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user.pro) {
|
||||
return () => null;
|
||||
}
|
||||
if (prevFilters && checkFilterEqual(filters, prevFilters)) {
|
||||
return () => null;
|
||||
}
|
||||
|
|
@ -61,9 +57,8 @@ function useFetchNotes(filters: Filters) {
|
|||
}
|
||||
|
||||
const Home: React.FunctionComponent<Props> = ({ location }) => {
|
||||
const { notes, user } = useSelector(state => {
|
||||
const { notes } = useSelector(state => {
|
||||
return {
|
||||
user: state.auth.user.data,
|
||||
notes: state.notes
|
||||
};
|
||||
});
|
||||
|
|
@ -80,24 +75,21 @@ const Home: React.FunctionComponent<Props> = ({ location }) => {
|
|||
>
|
||||
<HeadData filters={filters} />
|
||||
|
||||
<PayWall>
|
||||
<h1 className="sr-only">Notes</h1>
|
||||
<h1 className="sr-only">Notes</h1>
|
||||
|
||||
<Flash kind="danger" when={Boolean(notes.errorMessage)}>
|
||||
Error getting notes: {notes.errorMessage}
|
||||
</Flash>
|
||||
<Flash kind="danger" when={Boolean(notes.errorMessage)}>
|
||||
Error getting notes: {notes.errorMessage}
|
||||
</Flash>
|
||||
|
||||
<TopActions />
|
||||
<TopActions />
|
||||
|
||||
<NoteGroupList
|
||||
groups={groups}
|
||||
pro={user.pro}
|
||||
filters={filters}
|
||||
isFetched={notes.isFetched}
|
||||
/>
|
||||
<NoteGroupList
|
||||
groups={groups}
|
||||
filters={filters}
|
||||
isFetched={notes.isFetched}
|
||||
/>
|
||||
|
||||
{notes.data.length > 10 && <TopActions position="bottom" />}
|
||||
</PayWall>
|
||||
{notes.data.length > 10 && <TopActions position="bottom" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -41,8 +41,9 @@ SOFTWARE.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { IconProps } from './types';
|
||||
|
||||
const Icon = ({ fill, width, height, className }) => {
|
||||
const Icon = ({ fill, width, height, className }: IconProps) => {
|
||||
const h = `${height}px`;
|
||||
const w = `${width}px`;
|
||||
|
||||
|
|
@ -30,7 +30,6 @@ import { EditorSession, resetEditor } from '../../store/editor';
|
|||
import { setMessage } from '../../store/ui';
|
||||
import Editor from '../Common/Editor';
|
||||
import Flash from '../Common/Flash';
|
||||
import PayWall from '../Common/PayWall';
|
||||
import styles from './New.scss';
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
|
|
@ -78,72 +77,70 @@ const New: React.FunctionComponent<Props> = ({
|
|||
<title>New</title>
|
||||
</Helmet>
|
||||
|
||||
<PayWall>
|
||||
<div
|
||||
className={classnames(
|
||||
'page page-mobile-full container mobile-nopadding',
|
||||
styles.container
|
||||
)}
|
||||
>
|
||||
<Flash kind="danger" when={Boolean(errMessage)}>
|
||||
Error: {errMessage}
|
||||
</Flash>
|
||||
<div
|
||||
className={classnames(
|
||||
'page page-mobile-full container mobile-nopadding',
|
||||
styles.container
|
||||
)}
|
||||
>
|
||||
<Flash kind="danger" when={Boolean(errMessage)}>
|
||||
Error: {errMessage}
|
||||
</Flash>
|
||||
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.header}>
|
||||
<h2 className={styles.heading}>New notes</h2>
|
||||
</div>
|
||||
|
||||
<Editor
|
||||
isNew
|
||||
editor={editor}
|
||||
isBusy={submitting}
|
||||
textareaRef={textareaRef}
|
||||
bookSelectorTriggerRef={triggerRef}
|
||||
onSubmit={async ({ draftContent, draftBookUUID }) => {
|
||||
setSubmitting(true);
|
||||
|
||||
try {
|
||||
let bookUUID;
|
||||
|
||||
if (!draftBookUUID) {
|
||||
const book = await dispatch(createBook(editor.bookLabel));
|
||||
bookUUID = book.uuid;
|
||||
} else {
|
||||
bookUUID = draftBookUUID;
|
||||
}
|
||||
|
||||
const res = await operations.notes.create({
|
||||
bookUUID,
|
||||
content: draftContent
|
||||
});
|
||||
|
||||
dispatch(resetEditor(editor.sessionKey));
|
||||
|
||||
const dest = getNotePath(res.result.uuid);
|
||||
history.push(dest);
|
||||
|
||||
dispatch(
|
||||
setMessage({
|
||||
message: 'Created a note',
|
||||
kind: 'info',
|
||||
path: notePathDef
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
setErrMessage(err.message);
|
||||
setSubmitting(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.header}>
|
||||
<h2 className={styles.heading}>New notes</h2>
|
||||
</div>
|
||||
|
||||
<Prompt
|
||||
message="You have unsaved changes. Continue?"
|
||||
when={!persisted}
|
||||
<Editor
|
||||
isNew
|
||||
editor={editor}
|
||||
isBusy={submitting}
|
||||
textareaRef={textareaRef}
|
||||
bookSelectorTriggerRef={triggerRef}
|
||||
onSubmit={async ({ draftContent, draftBookUUID }) => {
|
||||
setSubmitting(true);
|
||||
|
||||
try {
|
||||
let bookUUID;
|
||||
|
||||
if (!draftBookUUID) {
|
||||
const book = await dispatch(createBook(editor.bookLabel));
|
||||
bookUUID = book.uuid;
|
||||
} else {
|
||||
bookUUID = draftBookUUID;
|
||||
}
|
||||
|
||||
const res = await operations.notes.create({
|
||||
bookUUID,
|
||||
content: draftContent
|
||||
});
|
||||
|
||||
dispatch(resetEditor(editor.sessionKey));
|
||||
|
||||
const dest = getNotePath(res.result.uuid);
|
||||
history.push(dest);
|
||||
|
||||
dispatch(
|
||||
setMessage({
|
||||
message: 'Created a note',
|
||||
kind: 'info',
|
||||
path: notePathDef
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
setErrMessage(err.message);
|
||||
setSubmitting(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</PayWall>
|
||||
|
||||
<Prompt
|
||||
message="You have unsaved changes. Continue?"
|
||||
when={!persisted}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import classnames from 'classnames';
|
|||
import {
|
||||
getNewRepetitionPath,
|
||||
getSettingsPath,
|
||||
getSubscriptionPath,
|
||||
SettingSections,
|
||||
repetitionsPathDef
|
||||
} from 'web/libs/paths';
|
||||
|
|
@ -37,29 +38,44 @@ const Content: React.FunctionComponent = () => {
|
|||
<div className={classnames('page-header', styles.header)}>
|
||||
<h1 className="page-heading">Repetition</h1>
|
||||
|
||||
<Link
|
||||
id="T-new-rule-btn"
|
||||
className="button button-first button-normal"
|
||||
to={getNewRepetitionPath()}
|
||||
>
|
||||
New
|
||||
</Link>
|
||||
{!user.pro ? (
|
||||
<button
|
||||
disabled
|
||||
type="button"
|
||||
className="button button-first button-normal"
|
||||
>
|
||||
New
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
id="T-new-rule-btn"
|
||||
className="button button-first button-normal"
|
||||
to={getNewRepetitionPath()}
|
||||
>
|
||||
New
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mobile-nopadding">
|
||||
<Flash when={!user.emailVerified} kind="warning">
|
||||
<Flash when={!user.pro} kind="warning">
|
||||
Repetitions are not enabled on your plan.{' '}
|
||||
<Link to={getSubscriptionPath()}>Upgrade here.</Link>
|
||||
</Flash>
|
||||
|
||||
<Flash when={user.pro && !user.emailVerified} kind="warning">
|
||||
Please verify your email address in order to receive digests.{' '}
|
||||
<Link to={getSettingsPath(SettingSections.account)}>
|
||||
Go to settings.
|
||||
</Link>
|
||||
</Flash>
|
||||
|
||||
<RepetitionList
|
||||
isFetching={repetitionRules.isFetching}
|
||||
isFetched={repetitionRules.isFetched}
|
||||
items={repetitionRules.data}
|
||||
setRuleUUIDToDelete={setRuleUUIDToDelete}
|
||||
pro={user.pro}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -16,13 +16,15 @@
|
|||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import classnames from 'classnames';
|
||||
import { RepetitionRuleData } from 'jslib/operations/types';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Helmet from 'react-helmet';
|
||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||
import { getRepetitionsPath } from 'web/libs/paths';
|
||||
|
||||
import { RepetitionRuleData } from 'jslib/operations/types';
|
||||
import services from 'web/libs/services';
|
||||
import { getRepetitionsPath } from 'web/libs/paths';
|
||||
import PayWall from '../../Common/PayWall';
|
||||
import { useDispatch } from '../../../store';
|
||||
import Flash from '../../Common/Flash';
|
||||
import repetitionStyles from '../Repetition.scss';
|
||||
|
|
@ -57,29 +59,31 @@ const EditRepetition: React.FunctionComponent<Props> = ({ match }) => {
|
|||
<title>Edit Repetition</title>
|
||||
</Helmet>
|
||||
|
||||
<div className="container mobile-fw">
|
||||
<div className={classnames('page-header', repetitionStyles.header)}>
|
||||
<h1 className="page-heading">Edit Repetition</h1>
|
||||
<PayWall>
|
||||
<div className="container mobile-fw">
|
||||
<div className={classnames('page-header', repetitionStyles.header)}>
|
||||
<h1 className="page-heading">Edit Repetition</h1>
|
||||
|
||||
<Link to={getRepetitionsPath()}>Back</Link>
|
||||
<Link to={getRepetitionsPath()}>Back</Link>
|
||||
</div>
|
||||
|
||||
<Flash
|
||||
kind="danger"
|
||||
when={errMsg !== ''}
|
||||
onDismiss={() => {
|
||||
setErrMsg('');
|
||||
}}
|
||||
>
|
||||
Error: {errMsg}
|
||||
</Flash>
|
||||
|
||||
{data === null ? (
|
||||
<div>loading</div>
|
||||
) : (
|
||||
<Content setErrMsg={setErrMsg} data={data} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Flash
|
||||
kind="danger"
|
||||
when={errMsg !== ''}
|
||||
onDismiss={() => {
|
||||
setErrMsg('');
|
||||
}}
|
||||
>
|
||||
Error: {errMsg}
|
||||
</Flash>
|
||||
|
||||
{data === null ? (
|
||||
<div>loading</div>
|
||||
) : (
|
||||
<Content setErrMsg={setErrMsg} data={data} />
|
||||
)}
|
||||
</div>
|
||||
</PayWall>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
|||
import classnames from 'classnames';
|
||||
|
||||
import { getRepetitionsPath, repetitionsPathDef } from 'web/libs/paths';
|
||||
import PayWall from '../../Common/PayWall';
|
||||
import {
|
||||
getRepetitionRules,
|
||||
createRepetitionRule
|
||||
|
|
@ -70,25 +71,27 @@ const NewRepetition: React.FunctionComponent<Props> = ({ history }) => {
|
|||
<title>New Repetition</title>
|
||||
</Helmet>
|
||||
|
||||
<div className="container mobile-fw">
|
||||
<div className={classnames('page-header', repetitionStyles.header)}>
|
||||
<h1 className="page-heading">New Repetition</h1>
|
||||
<PayWall>
|
||||
<div className="container mobile-fw">
|
||||
<div className={classnames('page-header', repetitionStyles.header)}>
|
||||
<h1 className="page-heading">New Repetition</h1>
|
||||
|
||||
<Link to={getRepetitionsPath()}>Back</Link>
|
||||
<Link to={getRepetitionsPath()}>Back</Link>
|
||||
</div>
|
||||
|
||||
<Flash
|
||||
kind="danger"
|
||||
when={errMsg !== ''}
|
||||
onDismiss={() => {
|
||||
setErrMsg('');
|
||||
}}
|
||||
>
|
||||
Error creating a rule: {errMsg}
|
||||
</Flash>
|
||||
|
||||
<Form onSubmit={handleSubmit} setErrMsg={setErrMsg} />
|
||||
</div>
|
||||
|
||||
<Flash
|
||||
kind="danger"
|
||||
when={errMsg !== ''}
|
||||
onDismiss={() => {
|
||||
setErrMsg('');
|
||||
}}
|
||||
>
|
||||
Error creating a rule: {errMsg}
|
||||
</Flash>
|
||||
|
||||
<Form onSubmit={handleSubmit} setErrMsg={setErrMsg} />
|
||||
</div>
|
||||
</PayWall>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,12 +27,14 @@ interface Props {
|
|||
isActive: boolean;
|
||||
onDelete: () => void;
|
||||
repetitionUUID: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Actions: React.FunctionComponent<Props> = ({
|
||||
isActive,
|
||||
onDelete,
|
||||
repetitionUUID
|
||||
repetitionUUID,
|
||||
disabled
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
|
|
@ -83,6 +85,7 @@ const Actions: React.FunctionComponent<Props> = ({
|
|||
setIsOpen={setIsOpen}
|
||||
isActive={isActive}
|
||||
options={options}
|
||||
disabled={disabled}
|
||||
optRefs={optRefs}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import styles from './RepetitionItem.scss';
|
|||
interface Props {
|
||||
item: RepetitionRuleData;
|
||||
setRuleUUIDToDelete: React.Dispatch<any>;
|
||||
pro: boolean;
|
||||
}
|
||||
|
||||
function formatLastActive(ms: number): string {
|
||||
|
|
@ -48,6 +49,7 @@ function formatNextActive(ms: number): string {
|
|||
|
||||
const RepetitionItem: React.FunctionComponent<Props> = ({
|
||||
item,
|
||||
pro,
|
||||
setRuleUUIDToDelete
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
|
@ -94,7 +96,7 @@ const RepetitionItem: React.FunctionComponent<Props> = ({
|
|||
<div className={styles.right}>
|
||||
<ul className={classnames('list-unstyled', styles['detail-list'])}>
|
||||
<li>
|
||||
{item.enabled ? (
|
||||
{item.enabled && item.nextActive !== 0 ? (
|
||||
<span>
|
||||
Scheduled in{' '}
|
||||
<Time
|
||||
|
|
@ -145,6 +147,7 @@ const RepetitionItem: React.FunctionComponent<Props> = ({
|
|||
setRuleUUIDToDelete(item.uuid);
|
||||
}}
|
||||
repetitionUUID={item.uuid}
|
||||
disabled={!pro}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -28,13 +28,15 @@ interface Props {
|
|||
isFetched: boolean;
|
||||
items: RepetitionRuleData[];
|
||||
setRuleUUIDToDelete: React.Dispatch<any>;
|
||||
pro: boolean;
|
||||
}
|
||||
|
||||
const ReptitionList: React.FunctionComponent<Props> = ({
|
||||
isFetching,
|
||||
isFetched,
|
||||
items,
|
||||
setRuleUUIDToDelete
|
||||
setRuleUUIDToDelete,
|
||||
pro
|
||||
}) => {
|
||||
if (isFetching && !isFetched) {
|
||||
return <div>loading</div>;
|
||||
|
|
@ -53,6 +55,7 @@ const ReptitionList: React.FunctionComponent<Props> = ({
|
|||
key={i.uuid}
|
||||
item={i}
|
||||
setRuleUUIDToDelete={setRuleUUIDToDelete}
|
||||
pro={pro}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
import PayWall from '../Common/PayWall';
|
||||
import Content from './Content';
|
||||
|
||||
const Repetition: React.FunctionComponent = () => {
|
||||
|
|
@ -29,9 +28,7 @@ const Repetition: React.FunctionComponent = () => {
|
|||
<title>Repetition</title>
|
||||
</Helmet>
|
||||
|
||||
<PayWall>
|
||||
<Content />
|
||||
</PayWall>
|
||||
<Content />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,11 +22,17 @@
|
|||
@import '../../App/rem';
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
padding-top: rem(52px);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.cvc,
|
||||
.expiry {
|
||||
width: rem(100px);
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { useDispatch } from '../../../store';
|
|||
import { setMessage } from '../../../store/ui';
|
||||
import NameOnCardInput from '../../Common/PaymentInput/NameOnCard';
|
||||
import CardInput from '../../Common/PaymentInput/Card';
|
||||
import Footer from '../Footer';
|
||||
import CountryInput from '../../Common/PaymentInput/Country';
|
||||
import styles from './Form.scss';
|
||||
|
||||
|
|
@ -116,7 +117,10 @@ const Form: React.FunctionComponent<Props> = ({
|
|||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<form className={classnames('container')} onSubmit={handleSubmit}>
|
||||
<form
|
||||
className={classnames('container', styles.form)}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Helmet>
|
||||
<title>Subscriptions</title>
|
||||
</Helmet>
|
||||
|
|
@ -174,6 +178,8 @@ const Form: React.FunctionComponent<Props> = ({
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ const perks = [
|
|||
{
|
||||
id: 'hosted',
|
||||
icon: <ServerIcon width={16} height={16} fill="#4d4d8b" />,
|
||||
value: 'Fully hosted and managed'
|
||||
value:
|
||||
'Maximize your memory retention and become the best learner you can be'
|
||||
},
|
||||
{
|
||||
id: 'support',
|
||||
|
|
|
|||
|
|
@ -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<Props> = ({ label }) => {
|
||||
return (
|
||||
<li className={styles.wrapper}>
|
||||
<CheckIcon width={16} height={16} fill="#0d5ec1" className="check-icon" />
|
||||
|
|
@ -31,3 +34,5 @@ export default ({ label }) => {
|
|||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureItem;
|
||||
|
|
@ -25,5 +25,4 @@
|
|||
list-style: none;
|
||||
margin-bottom: 0;
|
||||
padding-left: 0;
|
||||
padding-top: rem(12px);
|
||||
}
|
||||
|
|
@ -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<Props> = ({
|
||||
features,
|
||||
wrapperClassName
|
||||
}) => {
|
||||
return (
|
||||
<ul className={styles['feature-list']}>
|
||||
<ul className={classnames(styles['feature-list'], wrapperClassName)}>
|
||||
{features.map(feature => {
|
||||
return <FeatureItem key={feature.id} label={feature.label} />;
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default FeatureList;
|
||||
15
web/src/components/Subscription/Footer.tsx
Normal file
15
web/src/components/Subscription/Footer.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
|
||||
import styles from './Subscription.scss';
|
||||
|
||||
interface Props {}
|
||||
|
||||
const Footer: React.FunctionComponent<Props> = () => {
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
© 2019 Monomax Software Pty Ltd
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
|
@ -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: <BoxIcon width="16" height="16" fill="#6e6e6e" />,
|
||||
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<Props> = ({
|
||||
wrapperClassName,
|
||||
user,
|
||||
bottomContent
|
||||
}) => {
|
||||
return (
|
||||
<Plan
|
||||
name="Core"
|
||||
desc={desc}
|
||||
price="Free"
|
||||
perks={selfHostedPerks}
|
||||
wrapperClassName={wrapperClassName}
|
||||
ctaContent={ctaContent}
|
||||
ctaContent={
|
||||
<button
|
||||
type="button"
|
||||
className="button button-large button-second button-stretch"
|
||||
disabled
|
||||
>
|
||||
{user && user.pro ? 'Already upgraded!' : 'Your current plan'}
|
||||
</button>
|
||||
}
|
||||
bottomContent={bottomContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Core;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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: <ServerIcon width="16" height="16" fill="#4d4d8b" />,
|
||||
value: 'Fully hosted and managed'
|
||||
},
|
||||
{
|
||||
id: 'support',
|
||||
icon: <GlobeIcon width="16" height="16" fill="#4d4d8b" />,
|
||||
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<Props> = ({
|
||||
wrapperClassName,
|
||||
user,
|
||||
bottomContent
|
||||
}) => {
|
||||
return (
|
||||
<Plan
|
||||
name="Pro"
|
||||
price="$3"
|
||||
desc={desc}
|
||||
interval="month"
|
||||
perks={proPerks}
|
||||
wrapperClassName={wrapperClassName}
|
||||
ctaContent={ctaContent}
|
||||
ctaContent={<ProCTA user={user} />}
|
||||
bottomContent={bottomContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ProPlan;
|
||||
39
web/src/components/Subscription/Plan/ProCTA.tsx
Normal file
39
web/src/components/Subscription/Plan/ProCTA.tsx
Normal file
|
|
@ -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<Props> = ({ user }) => {
|
||||
if (user && user.pro) {
|
||||
return (
|
||||
<Link
|
||||
to={getSettingsPath(SettingSections.billing)}
|
||||
className="button button-large button-third-outline button-stretch"
|
||||
>
|
||||
Manage Your Plan
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
id="T-unlock-pro-btn"
|
||||
className={classnames('button button-large button-third button-stretch')}
|
||||
to={getSubscriptionCheckoutPath()}
|
||||
>
|
||||
Upgrade
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProCTA;
|
||||
|
|
@ -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<Props> = ({
|
||||
name,
|
||||
desc,
|
||||
price,
|
||||
bottomContent,
|
||||
ctaContent,
|
||||
interval,
|
||||
perks,
|
||||
wrapperClassName
|
||||
}) {
|
||||
}) => {
|
||||
return (
|
||||
<div className={classnames(styles.wrapper, wrapperClassName)}>
|
||||
<div
|
||||
|
|
@ -39,23 +55,14 @@ function Plan({
|
|||
>
|
||||
<h2 className={styles.name}>{name}</h2>
|
||||
|
||||
<ul className={classnames('list-unstyled', styles.perks)}>
|
||||
{perks.map(perk => {
|
||||
return (
|
||||
<li key={perk.id} className={styles['perk-item']}>
|
||||
<div className={styles['perk-icon']}>{perk.icon}</div>
|
||||
<div className={styles['perk-value']}>{perk.value}</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className={styles['price-wrapper']}>
|
||||
<strong className={styles.price}>{price}</strong>{' '}
|
||||
{interval && <div className={styles.interval}> / {interval}</div>}
|
||||
</div>
|
||||
|
||||
<p className={styles.desc}>{desc}</p>
|
||||
|
||||
<div className={styles['header-body']}>
|
||||
<div className={styles['price-wrapper']}>
|
||||
<strong className={styles.price}>{price}</strong>
|
||||
{interval && <div className={styles.interval}>/ {interval}</div>}
|
||||
</div>
|
||||
|
||||
<div className={styles['cta-wrapper']}>{ctaContent}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -63,6 +70,6 @@ function Plan({
|
|||
{bottomContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Plan;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: <div className={styles['feature-bold']}>Everything in core</div>
|
||||
},
|
||||
{
|
||||
id: 'host',
|
||||
label: <div>Hosting</div>
|
||||
},
|
||||
{
|
||||
id: 'auto',
|
||||
label: <div>Automatic update and migration</div>
|
||||
id: 'spaced-rep',
|
||||
label: <div>Automated Spaced Repetition</div>
|
||||
},
|
||||
{
|
||||
id: 'email-support',
|
||||
|
|
@ -48,7 +38,19 @@ const proFeatures = [
|
|||
}
|
||||
];
|
||||
|
||||
const baseFeatures = [
|
||||
const coreFeatures = [
|
||||
{
|
||||
id: 'oss',
|
||||
label: <div>Open source</div>
|
||||
},
|
||||
{
|
||||
id: 'num-notes',
|
||||
label: <div>Unlimited notes</div>
|
||||
},
|
||||
{
|
||||
id: 'num-books',
|
||||
label: <div>Unlimited books</div>
|
||||
},
|
||||
{
|
||||
id: 'sync',
|
||||
label: <div>Multi-device sync</div>
|
||||
|
|
@ -57,25 +59,13 @@ const baseFeatures = [
|
|||
id: 'cli',
|
||||
label: <div>Command line interface</div>
|
||||
},
|
||||
{
|
||||
id: 'atom',
|
||||
label: <div>Atom plugin</div>
|
||||
},
|
||||
{
|
||||
id: 'web',
|
||||
label: <div>Web client</div>
|
||||
},
|
||||
{
|
||||
id: 'digest',
|
||||
label: <div>Automated email digest</div>
|
||||
label: <div>Web application</div>
|
||||
},
|
||||
{
|
||||
id: 'ext',
|
||||
label: <div>Firefox/Chrome extension</div>
|
||||
},
|
||||
{
|
||||
id: 'foss',
|
||||
label: <div>Free and open source</div>
|
||||
label: <div>Chrome/Firefox extension</div>
|
||||
},
|
||||
{
|
||||
id: 'forum-support',
|
||||
|
|
@ -92,31 +82,6 @@ const Subscription: React.FunctionComponent<Props> = () => {
|
|||
};
|
||||
});
|
||||
|
||||
function renderPlanCta() {
|
||||
if (user && user.pro) {
|
||||
return (
|
||||
<Link
|
||||
to="/"
|
||||
className="button button-normal button-third-outline button-stretch"
|
||||
>
|
||||
Go to your notes
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
id="T-unlock-pro-btn"
|
||||
className={classnames(
|
||||
'button button-normal button-third button-stretch'
|
||||
)}
|
||||
to={getSubscriptionCheckoutPath()}
|
||||
>
|
||||
Unlock
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Helmet>
|
||||
|
|
@ -127,38 +92,45 @@ const Subscription: React.FunctionComponent<Props> = () => {
|
|||
/>
|
||||
</Helmet>
|
||||
|
||||
<div className={styles.hero}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.hero}>
|
||||
<div className="container">
|
||||
<h1 className={styles.heading}>Choose your Dnote plan.</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<h1 className={styles.heading}>
|
||||
You can self-host or sign up for the hosted version.
|
||||
</h1>
|
||||
<div className={styles['plans-wrapper']}>
|
||||
<CorePlan
|
||||
wrapperClassName={styles['core-plan']}
|
||||
user={user}
|
||||
bottomContent={
|
||||
<div className={styles.bottom}>
|
||||
<FeatureList features={coreFeatures} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<ProPlan
|
||||
wrapperClassName={styles['pro-plan']}
|
||||
user={user}
|
||||
bottomContent={
|
||||
<div className={styles.bottom}>
|
||||
<div className={styles['pro-prelude']}>
|
||||
Everything from the core plan, plus:
|
||||
</div>
|
||||
<FeatureList
|
||||
features={proFeatures}
|
||||
wrapperClassName={styles['pro-feature-list']}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container">
|
||||
<div className={styles['plans-wrapper']}>
|
||||
<CorePlan
|
||||
wrapperClassName={styles['core-plan']}
|
||||
ctaContent={
|
||||
<a
|
||||
href="https://github.com/dnote/dnote"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="button button-normal button-second-outline button-stretch"
|
||||
>
|
||||
See source code
|
||||
</a>
|
||||
}
|
||||
bottomContent={<FeatureList features={baseFeatures} />}
|
||||
/>
|
||||
|
||||
<ProPlan
|
||||
wrapperClassName={styles['pro-plan']}
|
||||
ctaContent={renderPlanCta()}
|
||||
bottomContent={<FeatureList features={proFeatures} />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue