diff --git a/README.md b/README.md index 470c8bc1..c76c6d6d 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,35 @@ # Dnote -A simple, encrypted personal knowledge base. +A simple notebook for developers. ## What It Does -Instantly capture your microlessons and get automatic reminders for spaced repetition. Because: - -* we forget exponentially unless we write down what we learn and come back. -* ideas cannot be grokked unless we can put them down in clear words. +Write technical notes and neatly organize them into books. ## How to Use -Use the following to keep Dnote handy. +You can use Dnote in a command line, web browser, or in an IDE. - [CLI](https://github.com/dnote/dnote/tree/master/cli) - [Web](https://dnote.io) - [Browser extension](https://github.com/dnote/browser-extension) - [Atom](https://github.com/dnote/dnote-atom) +It is designed to minimize switching environment. -### User Stories +## Privacy -- [How I Built a Personal Knowledge Base for Myself](https://dnote.io/blog/how-i-built-personal-knowledge-base-for-myself/) -- [I Wrote Down Everything I Learned While Programming for a Month](https://dnote.io/blog/writing-everything-i-learn-coding-for-a-month/) - -## Security - -Dnote is end-to-end encrypted and respects your privacy. It does not track you. - -When syncing, your data is encrypted with AES-256. Dnote server has zero knowledge about note contents. +Dnote is end-to-end encrypted with AES-256 to respect your privacy. It does not track you. ## Self-host Instructions are coming soon. +## User Stories + +- [How I Built a Personal Knowledge Base for Myself](https://dnote.io/blog/how-i-built-personal-knowledge-base-for-myself/) +- [I Wrote Down Everything I Learned While Programming for a Month](https://dnote.io/blog/writing-everything-i-learn-coding-for-a-month/) + ## Links - [Dnote](https://dnote.io) diff --git a/cli/README.md b/cli/README.md index 9a4c9cff..7666aa3f 100644 --- a/cli/README.md +++ b/cli/README.md @@ -4,7 +4,7 @@ A simple command line interface for Dnote. ![Dnote](assets/dnote.gif) -It is Designed to minimize context switching for taking notes. +It is Designed to minimize environment switching for taking notes. ## Install diff --git a/scripts/license.sh b/scripts/license.sh index b807818b..6f62e52f 100755 --- a/scripts/license.sh +++ b/scripts/license.sh @@ -64,7 +64,7 @@ done serverPath="$GOPATH"/src/github.com/dnote/dnote/server webPath="$GOPATH"/src/github.com/dnote/dnote/web - agplFiles=$(find "$serverPath" "$webPath" -type f \( -name "*.go" -o -name "*.js" \) ! -path "**/vendor/*" ! -path "**/node_modules/*") + agplFiles=$(find "$serverPath" "$webPath" -type f \( -name "*.go" -o -name "*.js" -o -name "*.scss" \) ! -path "**/vendor/*" ! -path "**/node_modules/*") for file in $agplFiles; do remove_notice "$file" diff --git a/server/Gopkg.lock b/server/Gopkg.lock index 4f3629fb..12db3dda 100644 --- a/server/Gopkg.lock +++ b/server/Gopkg.lock @@ -2,12 +2,12 @@ [[projects]] - digest = "1:9a88883f474d09f1da61894cd8115c7f33988d6941e4f6236324c777aaff8f2c" + digest = "1:439bfb51db599cd80766736b93b3d10e9314361197ada0b1209a51627a00ccd5" name = "github.com/PuerkitoBio/goquery" packages = ["."] pruneopts = "" - revision = "dc2ec5c7ca4d9aae063b79b9f581dd3ea6afd2b2" - version = "v1.4.1" + revision = "2d2796f41742ece03e8086188fa4db16a3a0b458" + version = "v1.5.0" [[projects]] digest = "1:e3726ad6f38f710e84c8dcd0e830014de6eaeea81f28d91ae898afecc078479a" @@ -30,12 +30,12 @@ version = "v0.2.0" [[projects]] - digest = "1:3dd078fda7500c341bc26cfbc6c6a34614f295a2457149fc1045cab767cbcf18" + digest = "1:529d738b7976c3848cae5cf3a8036440166835e389c1f617af701eeb12a0518d" name = "github.com/golang/protobuf" packages = ["proto"] pruneopts = "" - revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5" - version = "v1.2.0" + revision = "b5d812f8a3706043e23a9cd5babf2e5423744d30" + version = "v1.3.1" [[projects]] digest = "1:dbbeb8ddb0be949954c8157ee8439c2adfd8dc1c9510eb44a6e58cb68c3dce28" @@ -54,12 +54,12 @@ version = "v1.0.0" [[projects]] - digest = "1:c2c8666b4836c81a1d247bdf21c6a6fc1ab586538ab56f74437c2e0df5c375e1" + digest = "1:ec5262e6c65f9fcedc01d7789ee4e18ad429dde822cfd50e57e3fd77c4d70c73" name = "github.com/gorilla/mux" packages = ["."] pruneopts = "" - revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf" - version = "v1.6.2" + revision = "ed099d42384823742bba0bf9a72b53b55c9e2e38" + version = "v1.7.2" [[projects]] digest = "1:51a1d17ba9fe6648d9fdd5b822baa616d6ec7f28c8669c2091126d206796b5ae" @@ -78,12 +78,12 @@ version = "v1.1.3" [[projects]] - digest = "1:d269638dbd514822446c3c818b6389c880058af79ec54d97731818b34fe66921" + digest = "1:ac0cf20506076f1243a12c66052b862988915b1cd41881987addf0da946077c6" name = "github.com/jinzhu/gorm" packages = ["."] pruneopts = "" - revision = "6ed508ec6a4ecb3531899a69cbc746ccf65a4166" - version = "v1.9.1" + revision = "b7156195f7f3415f97c20abbd6aff894b847fee8" + version = "v1.9.8" [[projects]] branch = "master" @@ -102,18 +102,19 @@ version = "v1.3.0" [[projects]] - digest = "1:29145d7af4adafd72a79df5e41456ac9e232d5a28c1cd4dacf3ff008a217fc10" + digest = "1:a65d93f562c7d66d8db6f0c5df8ade208e0f42d9a9626924896697351ca161b3" name = "github.com/lib/pq" packages = [ ".", "oid", + "scram", ] pruneopts = "" - revision = "4ded0e9383f75c197b3a2aaa6d590ac52df6fd79" - version = "v1.0.0" + revision = "bc6a3c0594130b1e34005880bc600b6d3f49fa7f" + version = "v1.1.1" [[projects]] - digest = "1:8f5ef35477c8db09c49cc8f702dcc85bafcfc4e2f14b63975b587a589ce2303c" + digest = "1:38933a01adb848876d291f1d1415c942f131b859dd3910936ddddf72056f098f" name = "github.com/markbates/goth" packages = [ ".", @@ -122,16 +123,16 @@ "providers/gplus", ] pruneopts = "" - revision = "f9c6649ab984d6ea71ef1e13b7b1cdffcf4592d3" - version = "v1.46.1" + revision = "a3986c89570720dfc7c4b263e46a6d2c233d74cd" + version = "v1.52.0" [[projects]] - digest = "1:7365acd48986e205ccb8652cc746f09c8b7876030d53710ea6ef7d0bd0dcd7ca" + digest = "1:1d7e1867c49a6dd9856598ef7c3123604ea3daabf5b83f303ff457bcbc410b1d" name = "github.com/pkg/errors" packages = ["."] pruneopts = "" - revision = "645ef00459ed84a119197bfb8d8205042c6df63d" - version = "v0.8.0" + revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4" + version = "v0.8.1" [[projects]] digest = "1:6ab228f39a195cb1dab3564a0f27dc24a52bb3a19fa58dd2967f1e7b2482d82b" @@ -150,23 +151,24 @@ version = "v1.2.0" [[projects]] - digest = "1:424ed555c578339dc6d8ade516c6a2a6d3f461081aab905e955d240a2baed259" + digest = "1:f572b2f945e18823b755499b67aa2f9cac9404a02452f740a0dd39c8266fac62" name = "github.com/stripe/stripe-go" packages = [ ".", "card", "customer", "form", + "paymentsource", "sub", "webhook", ] pruneopts = "" - revision = "2288bfd59470171d308a3b823b0ff9722a81bcd7" - version = "v49.2.0" + revision = "557f3da8db5392aca3d47980cbee64aef9792c65" + version = "v60.17.0" [[projects]] branch = "master" - digest = "1:d263c39e9144265037f7473751f48b4193df3efe4763cc72e925c13d5bdbe551" + digest = "1:5b3e9450868bcf9ecbca2b01ac04f142255b5744d89ec97e1ceedf57d4522645" name = "golang.org/x/crypto" packages = [ "bcrypt", @@ -174,11 +176,11 @@ "pbkdf2", ] pruneopts = "" - revision = "7c1a557ab941a71c619514f229f0b27ccb0c27cf" + revision = "22d7a77e9e5f409e934ed268692e56707cd169e5" [[projects]] branch = "master" - digest = "1:3a3c1b660248c0ec25f00cfb9c6526bd5b0ede4c8bfa2ed56a3f5e7e9d0a19cd" + digest = "1:aa38821ad1406a84f9577465ef53e56ca4d90745a710c753190bf7792d726c82" name = "golang.org/x/net" packages = [ "context", @@ -187,29 +189,29 @@ "html/atom", ] pruneopts = "" - revision = "146acd28ed5894421fb5aac80ca93bc1b1f46f87" + revision = "3ec19112720433827bbce8be9342797f5a6aaaf9" [[projects]] branch = "master" - digest = "1:235cb00e80dcf85b78a24be4bbe6c827fb28613b84037a9d524084308a849d91" + digest = "1:348696484a568aa816b0aa29d4924afa1a4e5492e29a003eaf365f650a53c7b4" name = "golang.org/x/oauth2" packages = [ ".", "internal", ] pruneopts = "" - revision = "c57b0facaced709681d9f90397429b9430a74754" + revision = "9f3314589c9a9136388751d9adae6b0ed400978a" [[projects]] branch = "master" - digest = "1:55a681cb66f28755765fa5fa5104cbd8dc85c55c02d206f9f89566451e3fe1aa" + digest = "1:9522af4be529c108010f95b05f1022cb872f2b9ff8b101080f554245673466e1" name = "golang.org/x/time" packages = ["rate"] pruneopts = "" - revision = "fbb02b2291d28baffd63558aa44b4b56f178d650" + revision = "9d24e82272b4f38b78bc8cff74fa936d31ccd8ef" [[projects]] - digest = "1:8c432632a230496c35a15cfdf441436f04c90e724ad99c8463ef0c82bbe93edb" + digest = "1:01b9b21ce3c29e95c6226188ab77233e59f4e397262a078cd6f248405b86dda7" name = "google.golang.org/appengine" packages = [ "internal", @@ -221,8 +223,8 @@ "urlfetch", ] pruneopts = "" - revision = "ae0ab99deb4dc413a2b4bd6c8bdd0eb67f1e4d06" - version = "v1.2.0" + revision = "4c25cacc810c02874000e4f7071286a8e96b2515" + version = "v1.6.0" [[projects]] branch = "v3" @@ -261,6 +263,7 @@ "github.com/stripe/stripe-go", "github.com/stripe/stripe-go/card", "github.com/stripe/stripe-go/customer", + "github.com/stripe/stripe-go/paymentsource", "github.com/stripe/stripe-go/sub", "github.com/stripe/stripe-go/webhook", "golang.org/x/crypto/bcrypt", diff --git a/server/Gopkg.toml b/server/Gopkg.toml index f0d88ab3..f7879bb9 100644 --- a/server/Gopkg.toml +++ b/server/Gopkg.toml @@ -67,7 +67,7 @@ [[constraint]] name = "github.com/stripe/stripe-go" - version = "49.1.0" + version = "60.17.0" [[constraint]] name = "gopkg.in/gomail.v2" diff --git a/server/api/handlers/helpers.go b/server/api/handlers/helpers.go index b3b730a1..cc2f26a4 100644 --- a/server/api/handlers/helpers.go +++ b/server/api/handlers/helpers.go @@ -21,8 +21,10 @@ package handlers import ( crand "crypto/rand" "encoding/base64" + "net/http" "strings" + "github.com/dnote/dnote/server/api/logger" "github.com/dnote/dnote/server/database" "github.com/jinzhu/gorm" "github.com/pkg/errors" @@ -104,3 +106,11 @@ func getClientType(origin string) string { return "web" } + +// handleError logs the error and responds with the given status code with a generic status text +func handleError(w http.ResponseWriter, logMessage string, err error, statusCode int) { + logger.Err("[%d] %s: %v\n", statusCode, logMessage, err) + + statusText := http.StatusText(statusCode) + http.Error(w, statusText, statusCode) +} diff --git a/server/api/handlers/routes.go b/server/api/handlers/routes.go index 0036f88f..ebc28ea0 100644 --- a/server/api/handlers/routes.go +++ b/server/api/handlers/routes.go @@ -315,7 +315,7 @@ func applyMiddleware(h http.Handler, rateLimit bool) http.Handler { // App is an application configuration type App struct { Clock clock.Clock - StripeAPIBackend *stripe.BackendImplementation + StripeAPIBackend stripe.Backend } // init sets up the application based on the configuration diff --git a/server/api/handlers/subscription.go b/server/api/handlers/subscription.go index 692cd6e3..5a0614da 100644 --- a/server/api/handlers/subscription.go +++ b/server/api/handlers/subscription.go @@ -20,75 +20,150 @@ package handlers import ( "encoding/json" + "fmt" "io/ioutil" "net/http" "os" + "strings" "github.com/dnote/dnote/server/api/helpers" - "github.com/dnote/dnote/server/api/logger" "github.com/dnote/dnote/server/api/operations" "github.com/dnote/dnote/server/database" + "github.com/jinzhu/gorm" "github.com/pkg/errors" "github.com/stripe/stripe-go" "github.com/stripe/stripe-go/card" "github.com/stripe/stripe-go/customer" + "github.com/stripe/stripe-go/paymentsource" + "github.com/stripe/stripe-go/source" "github.com/stripe/stripe-go/sub" "github.com/stripe/stripe-go/webhook" ) -type stripeToken struct { - ID string `json:"id"` - Email string `json:"email"` +var proPlanID = "plan_EpgsEvY27pajfo" + +func getOrCreateStripeCustomer(tx *gorm.DB, user database.User) (*stripe.Customer, error) { + if user.StripeCustomerID != "" { + c, err := customer.Get(user.StripeCustomerID, nil) + if err != nil { + return nil, errors.Wrap(err, "getting customer") + } + + return c, nil + } + + var account database.Account + if err := tx.Where("user_id = ?", user.ID).First(&account).Error; err != nil { + return nil, errors.Wrap(err, "finding account") + } + + customerParams := &stripe.CustomerParams{ + Email: &account.Email.String, + } + c, err := customer.New(customerParams) + if err != nil { + return nil, errors.Wrap(err, "creating customer") + } + + user.StripeCustomerID = c.ID + if err := tx.Save(&user).Error; err != nil { + return nil, errors.Wrap(err, "updating user") + } + + return c, nil } -var planID = "plan_EpgsEvY27pajfo" +func addCustomerSource(customerID, sourceID string) (*stripe.PaymentSource, error) { + params := &stripe.CustomerSourceParams{ + Customer: stripe.String(customerID), + Source: &stripe.SourceParams{ + Token: stripe.String(sourceID), + }, + } -func init() { + src, err := paymentsource.New(params) + if err != nil { + return nil, errors.Wrap(err, "creating source for customer") + } + + return src, nil +} + +func createCustomerSubscription(customerID, planID string) (*stripe.Subscription, error) { + subParams := &stripe.SubscriptionParams{ + Customer: stripe.String(customerID), + Items: []*stripe.SubscriptionItemsParams{ + { + Plan: stripe.String(planID), + }, + }, + } + + s, err := sub.New(subParams) + if err != nil { + return nil, errors.Wrap(err, "creating subscription for customer") + } + + return s, nil +} + +type createSubPayload struct { + Source stripe.Source `json:"source"` + Country string `json:"country"` } // createSub creates a subscription for a the current user func (a *App) createSub(w http.ResponseWriter, r *http.Request) { - db := database.DBConn - user, ok := r.Context().Value(helpers.KeyUser).(database.User) if !ok { http.Error(w, "No authenticated user found", http.StatusInternalServerError) return } - if user.StripeCustomerID != "" { - http.Error(w, "Customer already exists", http.StatusForbidden) + + var payload createSubPayload + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + handleError(w, "decoding params", err, http.StatusBadRequest) return } - var tok stripeToken - if err := json.NewDecoder(r.Body).Decode(&tok); err != nil { - http.Error(w, errors.Wrap(err, "decoding params").Error(), http.StatusInternalServerError) + db := database.DBConn + tx := db.Begin() + + if err := tx.Model(&user). + Update(map[string]interface{}{ + "cloud": true, + "billing_country": payload.Country, + }).Error; err != nil { + tx.Rollback() + handleError(w, "updating user", err, http.StatusInternalServerError) return } - customerParams := &stripe.CustomerParams{ - Plan: &planID, - Email: &tok.Email, - } - err := customerParams.SetSource(tok.ID) + customer, err := getOrCreateStripeCustomer(tx, user) if err != nil { - http.Error(w, errors.Wrap(err, "setting source").Error(), http.StatusInternalServerError) + tx.Rollback() + handleError(w, "getting customer", err, http.StatusInternalServerError) return } - //TODO: if customer exists, update not create - c, err := customer.New(customerParams) - if err != nil { - http.Error(w, errors.Wrap(err, "creating customer").Error(), http.StatusInternalServerError) + if _, err = addCustomerSource(customer.ID, payload.Source.ID); err != nil { + tx.Rollback() + handleError(w, "attaching source", err, http.StatusInternalServerError) return } - user.StripeCustomerID = c.ID - user.Cloud = true - if err := db.Save(&user).Error; err != nil { - http.Error(w, errors.Wrap(err, "updating user").Error(), http.StatusInternalServerError) + if _, err := createCustomerSubscription(customer.ID, proPlanID); err != nil { + tx.Rollback() + handleError(w, "creating subscription", err, http.StatusInternalServerError) return } + + if err := tx.Commit().Error; err != nil { + handleError(w, "committing a subscription transaction", err, http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) } type updateSubPayload struct { @@ -141,12 +216,11 @@ func (a *App) updateSub(w http.ResponseWriter, r *http.Request) { var payload updateSubPayload if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - http.Error(w, errors.Wrap(err, "decoding params").Error(), http.StatusInternalServerError) + handleError(w, "decoding params", err, http.StatusBadRequest) return } - if err := validateUpdateSubPayload(payload); err != nil { - http.Error(w, errors.Wrap(err, "invalid payload").Error(), http.StatusBadRequest) + handleError(w, "invalid payload", err, http.StatusBadRequest) return } @@ -165,7 +239,7 @@ func (a *App) updateSub(w http.ResponseWriter, r *http.Request) { statusCode = http.StatusInternalServerError } - http.Error(w, errors.Wrapf(err, "during operation %s", payload.Op).Error(), statusCode) + handleError(w, fmt.Sprintf("during operation %s", payload.Op), err, statusCode) return } @@ -189,13 +263,13 @@ type GetSubResponse struct { } func respondWithEmptySub(w http.ResponseWriter) { - emptyGetSubREsponse := GetSubResponse{ + emptyGetSubResponse := GetSubResponse{ Items: []GetSubResponseItem{}, } w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(emptyGetSubREsponse); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + if err := json.NewEncoder(w).Encode(emptyGetSubResponse); err != nil { + handleError(w, "encoding response", err, http.StatusInternalServerError) return } } @@ -218,7 +292,7 @@ func (a *App) getSub(w http.ResponseWriter, r *http.Request) { if !i.Next() { if err := i.Err(); err != nil { - http.Error(w, errors.Wrap(err, "fetching subscription").Error(), http.StatusInternalServerError) + handleError(w, "fetching subscription", err, http.StatusInternalServerError) return } @@ -247,7 +321,7 @@ func (a *App) getSub(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(resp); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + handleError(w, "encoding response", err, http.StatusInternalServerError) return } } @@ -262,13 +336,64 @@ type GetStripeSourceResponse struct { func respondWithEmptyStripeToken(w http.ResponseWriter) { var resp GetStripeSourceResponse + w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(resp); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + handleError(w, "encoding response", err, http.StatusInternalServerError) return } } +// getStripeCard retrieves card information from stripe and returns a stripe.Card +// It handles legacy 'card' resource which have 'card_' prefixes, as well as the +// more up-to-date 'source' resources which have 'src_' prefixes. +func getStripeCard(stripeCustomerID, sourceID string) (*stripe.Card, error) { + if strings.HasPrefix(sourceID, "card_") { + params := &stripe.CardParams{ + Customer: stripe.String(stripeCustomerID), + } + cd, err := card.Get(sourceID, params) + if err != nil { + return nil, errors.Wrap(err, "fetching card") + } + + return cd, nil + } else if strings.HasPrefix(sourceID, "src_") { + src, err := source.Get(sourceID, nil) + if err != nil { + return nil, errors.Wrap(err, "fetching source") + } + + brand, ok := src.TypeData["brand"].(string) + if !ok { + return nil, errors.New("casting brand") + } + last4, ok := src.TypeData["last4"].(string) + if !ok { + return nil, errors.New("casting last4") + } + expMonth, ok := src.TypeData["exp_month"].(float64) + if !ok { + return nil, errors.New("casting exp_month") + } + expYear, ok := src.TypeData["exp_year"].(float64) + if !ok { + return nil, errors.New("casting exp_year") + } + + cd := &stripe.Card{ + Brand: stripe.CardBrand(brand), + Last4: last4, + ExpMonth: uint8(expMonth), + ExpYear: uint16(expYear), + } + + return cd, nil + } + + return nil, errors.Errorf("malformed sourceID %s", sourceID) +} + func (a *App) getStripeSource(w http.ResponseWriter, r *http.Request) { user, ok := r.Context().Value(helpers.KeyUser).(database.User) if !ok { @@ -282,20 +407,18 @@ func (a *App) getStripeSource(w http.ResponseWriter, r *http.Request) { c, err := customer.Get(user.StripeCustomerID, nil) if err != nil { - http.Error(w, errors.Wrap(err, "fetching stripe customer").Error(), http.StatusInternalServerError) + handleError(w, "fetching stripe customer", err, http.StatusInternalServerError) return } + if c.DefaultSource == nil { respondWithEmptyStripeToken(w) return } - params := &stripe.CardParams{ - Customer: stripe.String(user.StripeCustomerID), - } - cd, err := card.Get(c.DefaultSource.ID, params) + cd, err := getStripeCard(user.StripeCustomerID, c.DefaultSource.ID) if err != nil { - http.Error(w, errors.Wrap(err, "fetching stripe card").Error(), http.StatusInternalServerError) + handleError(w, "fetching stripe source", err, http.StatusInternalServerError) return } @@ -308,7 +431,7 @@ func (a *App) getStripeSource(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(resp); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + handleError(w, "encoding response", err, http.StatusInternalServerError) return } } @@ -316,16 +439,14 @@ func (a *App) getStripeSource(w http.ResponseWriter, r *http.Request) { func (a *App) stripeWebhook(w http.ResponseWriter, req *http.Request) { body, err := ioutil.ReadAll(req.Body) if err != nil { - logger.Err("Error reading request body: %v\n", err) - w.WriteHeader(http.StatusServiceUnavailable) + handleError(w, "reading body", err, http.StatusServiceUnavailable) return } webhookSecret := os.Getenv("StripeWebhookSecret") event, err := webhook.ConstructEvent(body, req.Header.Get("Stripe-Signature"), webhookSecret) if err != nil { - logger.Err("Error verifying the signature: %v\n", err) - w.WriteHeader(http.StatusBadRequest) + handleError(w, "verifying stripe webhook signature", err, http.StatusBadRequest) return } @@ -334,8 +455,7 @@ func (a *App) stripeWebhook(w http.ResponseWriter, req *http.Request) { { var subscription stripe.Subscription if json.Unmarshal(event.Data.Raw, &subscription); err != nil { - logger.Err(errors.Wrap(err, "unmarshaling").Error()) - w.WriteHeader(http.StatusBadRequest) + handleError(w, "unmarshaling payload", err, http.StatusBadRequest) return } @@ -343,8 +463,8 @@ func (a *App) stripeWebhook(w http.ResponseWriter, req *http.Request) { } default: { - logger.Err("Unsupported webhook event type %s", event.Type) - w.WriteHeader(http.StatusBadRequest) + msg := fmt.Sprintf("Unsupported webhook event type %s", event.Type) + handleError(w, msg, err, http.StatusBadRequest) return } } diff --git a/server/database/models.go b/server/database/models.go index 8b50c829..f8ff0ac5 100644 --- a/server/database/models.go +++ b/server/database/models.go @@ -67,6 +67,7 @@ type User struct { APIKey string `json:"-" gorm:"index"` Name string `json:"name"` StripeCustomerID string `json:"-"` + BillingCountry string `json:"-"` Cloud bool `json:"-" gorm:"default:false"` IsAdmin bool `json:"-" gorm:"default:false"` Account Account diff --git a/server/testutils/main.go b/server/testutils/main.go index 211cadec..301a37e2 100644 --- a/server/testutils/main.go +++ b/server/testutils/main.go @@ -349,15 +349,25 @@ func GetCookieByName(cookies []*http.Cookie, name string) *http.Cookie { return ret } -// CreateMockStripeBackend returns a mock stripe backend implementation that uses +// CreateMockStripeBackend returns a mock stripe backend that uses // the given test server -func CreateMockStripeBackend(ts *httptest.Server) *stripe.BackendImplementation { - c := ts.Client() - bi := stripe.BackendImplementation{ - Type: stripe.APIBackend, - URL: ts.URL + "/v1", - HTTPClient: c, - } +func CreateMockStripeBackend(ts *httptest.Server) stripe.Backend { + stripeMockBackend := stripe.GetBackendWithConfig( + stripe.APIBackend, + &stripe.BackendConfig{ + URL: ts.URL + "/v1", + HTTPClient: ts.Client(), + }, + ) - return &bi + return stripeMockBackend +} + +// MustRespondJSON responds with the JSON-encoding of the given interface. If the encoding +// fails, the test fails. It is used by test servers. +func MustRespondJSON(t *testing.T, w http.ResponseWriter, i interface{}, message string) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(i); err != nil { + t.Fatal(message) + } } diff --git a/web/.eslintrc b/web/.eslintrc index 335ed7d9..bbaaf9f7 100644 --- a/web/.eslintrc +++ b/web/.eslintrc @@ -53,6 +53,7 @@ "__DOMAIN__": true, "__BASE_URL__": true, "__BASE_NAME__": true, + "__STRIPE_PUBLIC_KEY__": true, "socket": true, "webpackIsomorphicTools": true, "StripeCheckout": true diff --git a/web/package-lock.json b/web/package-lock.json index be9724ce..b5b4be16 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -6531,6 +6531,12 @@ "path-is-inside": "^1.0.2" } }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -7469,14 +7475,28 @@ } }, "mini-css-extract-plugin": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz", - "integrity": "sha512-IuaLjruM0vMKhUUT51fQdQzBYTX49dLj8w68ALEAe2A4iYNpIC4eMac67mt3NzycvjOlf07/kYxJDc0RTl1Wqw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.6.0.tgz", + "integrity": "sha512-79q5P7YGI6rdnVyIAV4NXpBQJFWdkzJxCim3Kog4078fM0piAaFlwocqbejdWtLW1cEzCexPrh6EdyFsPgVdAw==", "dev": true, "requires": { "loader-utils": "^1.1.0", + "normalize-url": "^2.0.1", "schema-utils": "^1.0.0", "webpack-sources": "^1.1.0" + }, + "dependencies": { + "normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "dev": true, + "requires": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + } + } } }, "minimalistic-assert": { @@ -9239,6 +9259,12 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true + }, "prettier": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.17.1.tgz", @@ -9391,6 +9417,17 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "dev": true, + "requires": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", @@ -9595,6 +9632,14 @@ "shallowequal": "^1.0.1" } }, + "react-stripe-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-stripe-elements/-/react-stripe-elements-3.0.0.tgz", + "integrity": "sha512-ouGHPmPhdg7KUTOFVuYIr0UWVgBjG5CqmOn9/y0vaJICryPB6llkiOGeUXKrac7fu1/vtPdn8t/4JKnbi8PT8g==", + "requires": { + "prop-types": "^15.5.10" + } + }, "react-tooltip": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-3.10.0.tgz", @@ -10861,6 +10906,15 @@ } } }, + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -11131,6 +11185,12 @@ "lodash": "^4.17.10" } }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", diff --git a/web/package.json b/web/package.json index f4126bc6..b4f8c99a 100644 --- a/web/package.json +++ b/web/package.json @@ -73,6 +73,7 @@ "react-router": "^5.0.0", "react-router-config": "^5.0.0", "react-router-dom": "^5.0.0", + "react-stripe-elements": "^3.0.0", "react-tooltip": "^3.10.0", "redux": "^4.0.0", "redux-thunk": "^2.1.0", diff --git a/web/src/components/App/module.scss b/web/src/components/App/App.global.scss similarity index 100% rename from web/src/components/App/module.scss rename to web/src/components/App/App.global.scss diff --git a/web/src/components/App/App.module.scss b/web/src/components/App/App.module.scss index 8632a128..8f4dfe9b 100644 --- a/web/src/components/App/App.module.scss +++ b/web/src/components/App/App.module.scss @@ -1,4 +1,4 @@ -@import '../App/variables'; +@import '../App/theme'; @import '../App/responsive'; @import '../App/rem'; diff --git a/web/src/components/App/_buttons.scss b/web/src/components/App/_buttons.scss index c85d968b..f65ff404 100644 --- a/web/src/components/App/_buttons.scss +++ b/web/src/components/App/_buttons.scss @@ -1,4 +1,4 @@ -@import './variables'; +@import './theme'; @import './rem'; @import './font'; @@ -28,7 +28,6 @@ .button { position: relative; display: inline-block; - line-height: 1.25; text-align: center; white-space: nowrap; vertical-align: middle; @@ -44,8 +43,6 @@ cursor: pointer; &:not(.button-no-ui) { - padding: rem(8px) rem(16px); - @include font-size('small'); } &:not(:disabled):hover { @@ -67,9 +64,36 @@ button:disabled { opacity: 0.6; } +.button-normal { + @include font-size('small'); + padding: rem(8px) rem(16px); +} + .button-large { - &:not(.button-no-ui) { - padding: rem(12px) rem(16px); + @include font-size('medium'); + + padding: rem(8px) rem(24px); + + @include breakpoint(md) { + padding: rem(12px) rem(36px); + } + + @include breakpoint(lg) { + padding: rem(12px) rem(48px); + } +} + +.button-xlarge { + @include font-size('x-large'); + + padding: rem(16px) rem(24px); + + @include breakpoint(md) { + padding: rem(12px) rem(36px); + } + + @include breakpoint(lg) { + padding: rem(16px) rem(48px); } } @@ -107,7 +131,7 @@ button:disabled { } .button ~ .button { - margin-left: rem(8px); + margin-left: rem(12px); } .button-no-ui { diff --git a/web/src/components/App/_font.scss b/web/src/components/App/_font.scss index b6aee859..01807ddb 100644 --- a/web/src/components/App/_font.scss +++ b/web/src/components/App/_font.scss @@ -1,57 +1,81 @@ @import './responsive'; +$lowDecay: 0.1; +$medDecay: 0.15; +$highDecay: 0.2; + // font-size is a mixin for pre-defined font-size values in rem. // It also includes px as a fallback for older browsers. @mixin font-size($size, $responsive: true) { - $sizeValue: 16; + $smSizeValue: 16; $mdSizeValue: 16; $lgSizeValue: 16; @if $size == 'x-small' { - $sizeValue: 10; - $mdSizeValue: 10; - $lgSizeValue: 10; + $baseSize: 10; + + $smSizeValue: $baseSize; + $mdSizeValue: $baseSize; + $lgSizeValue: $baseSize; } @else if $size == 'small' { - $sizeValue: 14; - $mdSizeValue: 14; - $lgSizeValue: 14; + $baseSize: 14; + + $smSizeValue: $baseSize; + $mdSizeValue: $baseSize; + $lgSizeValue: $baseSize; } @else if $size == 'regular' { - $sizeValue: 16; - $mdSizeValue: 16; - $lgSizeValue: 16; + $baseSize: 16; + + $smSizeValue: $baseSize * (1 - $lowDecay); + $mdSizeValue: $baseSize * (1 - $lowDecay); + $lgSizeValue: $baseSize; } @else if $size == 'medium' { - $sizeValue: 16; - $mdSizeValue: 18; - $lgSizeValue: 18; + $baseSize: 18; + + $smSizeValue: $baseSize; + $mdSizeValue: $baseSize; + $lgSizeValue: $baseSize; } @else if $size == 'large' { - $sizeValue: 18; - $mdSizeValue: 18; - $lgSizeValue: 20; + $baseSize: 20; + + $smSizeValue: $baseSize; + $mdSizeValue: $baseSize; + $lgSizeValue: $baseSize; } @else if $size == 'x-large' { - $sizeValue: 22; - $mdSizeValue: 22; - $lgSizeValue: 24; + $baseSize: 24; + + $smSizeValue: $baseSize * (1 - $lowDecay * 2); + $mdSizeValue: $baseSize * (1 - $lowDecay); + $lgSizeValue: $baseSize; } @else if $size == '2x-large' { - $sizeValue: 20; - $mdSizeValue: 24; - $lgSizeValue: 32; + $baseSize: 32; + + $smSizeValue: $baseSize * (1 - $lowDecay * 2); + $mdSizeValue: $baseSize * (1 - $lowDecay); + $lgSizeValue: $baseSize; } @else if $size == '3x-large' { - $sizeValue: 24; - $mdSizeValue: 32; - $lgSizeValue: 36; + $baseSize: 36; + + $smSizeValue: $baseSize * (1 - $medDecay * 2); + $mdSizeValue: $baseSize * (1 - $medDecay); + $lgSizeValue: $baseSize; } @else if $size == '4x-large' { - $sizeValue: 32; - $mdSizeValue: 32; - $lgSizeValue: 48; + $baseSize: 48; + + $smSizeValue: $baseSize * (1 - $medDecay * 2); + $mdSizeValue: $baseSize * (1 - $medDecay); + $lgSizeValue: $baseSize; } @else if $size == '5x-large' { - $sizeValue: 32; - $mdSizeValue: 36; - $lgSizeValue: 56; + $baseSize: 56; + + $smSizeValue: $baseSize * (1 - $highDecay * 2); + $mdSizeValue: $baseSize * (1 - $highDecay); + $lgSizeValue: $baseSize; } @if $responsive == true { - font-size: $sizeValue * 1px; - font-size: $sizeValue * 0.1rem; + font-size: $smSizeValue * 1px; + font-size: $smSizeValue * 0.1rem; @include breakpoint(md) { font-size: $mdSizeValue * 1px; @@ -63,7 +87,7 @@ font-size: $lgSizeValue * 0.1rem; } } @else { - font-size: $sizeValue * 1px; - font-size: $sizeValue * 0.1rem; + font-size: $lgSizeValue * 1px; + font-size: $lgSizeValue * 0.1rem; } } diff --git a/web/src/components/App/_grid.scss b/web/src/components/App/_grid.scss index 2d93dd69..ab8ed3ce 100644 --- a/web/src/components/App/_grid.scss +++ b/web/src/components/App/_grid.scss @@ -15,6 +15,58 @@ html { box-sizing: inherit; } +.container-wide { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container-wide { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container-wide { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container-wide { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container-wide { + max-width: 1040px; + } +} + +@media (min-width: 1440px) { + .container-wide { + max-width: 1180px; + } +} + +@media (min-width: 1800px) { + .container-wide { + max-width: 1660px; + } +} + +.container-fluid { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + .container { width: 100%; padding-right: 15px; @@ -43,30 +95,10 @@ html { @media (min-width: 1200px) { .container { - max-width: 1040px; + max-width: 1140px; } } -@media (min-width: 1440px) { - .container { - max-width: 1180px; - } -} - -@media (min-width: 1800px) { - .container { - max-width: 1660px; - } -} - -.container-fluid { - width: 100%; - padding-right: 15px; - padding-left: 15px; - margin-right: auto; - margin-left: auto; -} - .row { display: -ms-flexbox; display: flex; diff --git a/web/src/components/App/_responsive.scss b/web/src/components/App/_responsive.scss index b5f6e90e..63c24eda 100644 --- a/web/src/components/App/_responsive.scss +++ b/web/src/components/App/_responsive.scss @@ -1,5 +1,8 @@ +@import './variables'; + $xl-breakpoint: 1441px; -$lg-breakpoint: 1280px; +//$lg-breakpoint: 1280px; +$lg-breakpoint: 992px; //$md-breakpoint: 600px; $md-breakpoint: 768px; $sm-breakpoint: 321px; @@ -39,3 +42,11 @@ $sm-breakpoint: 321px; } } } + +// overSidebarThreshold is a mixin that applies the given style if and only if +// the current viewport width is above the sidebar threshold +@mixin overSidebarThreshold() { + @media (min-width: $note-sidebar-threshold) { + @content; + } +} diff --git a/web/src/components/App/_shared.scss b/web/src/components/App/_shared.scss index 8aad21e3..8bfc8ea6 100644 --- a/web/src/components/App/_shared.scss +++ b/web/src/components/App/_shared.scss @@ -18,111 +18,6 @@ background: #f4f4f4; } -.alert { - // display: flex; - // flex-direction: column; - // align-items: flex-start; - margin-bottom: 0; - - @include breakpoint(md) { - // flex-direction: row; - // align-items: center; - // justify-content: space-between; - } - - .alert-content { - margin-bottom: 0; - } - - .alert-action { - margin-top: 10px; - - @include breakpoint(md) { - margin-top: 0; - } - } -} - -// panels -.panel { - background-color: white; - list-style: none; - padding-left: 0; - margin-bottom: 0; - border-radius: 3px; - word-wrap: break-word; - - .panel-heading { - font-size: 2rem; - margin-bottom: 10px; - } -} - -.panel-padded { - padding: 16px 25px; -} - -.panel ~ .panel { - margin-top: 12px; -} - -// dropdown -.dropdown-content { - position: absolute; - list-style: none; - padding: 0; - margin-bottom: 0; - background-color: #ffffff; - color: #000000; - box-shadow: 0 0 4px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.29); - border-radius: 1.5px; - z-index: 1; - - .dropdown-content-header { - padding: 8px 14px; - display: block; - margin-bottom: 0; - font-size: 1.4rem; - color: #868e96; - white-space: nowrap; - } - - .dropdown-content-divider { - height: 0; - overflow: hidden; - border-top: 1px solid #e9ecef; - } - - &::after { - top: 0px; - border: 7px solid transparent; - border-bottom-color: #fff; - position: absolute; - display: inline-block; - content: ''; - } - - &::before { - top: -2px; - border: 8px solid transparent; - border-bottom-color: rgba(27, 31, 35, 0.15); - position: absolute; - display: inline-block; - content: ''; - } -} - -// dismissable - -.dismissable-overlay { - position: fixed; - top: 0px; - left: 0px; - right: 0px; - bottom: 0px; - // z-index: 2; -} - input[type='text']:disabled, input[type='email']:disabled, input[type='number']:disabled, @@ -131,14 +26,13 @@ textarea:disabled { background: #ececec; cursor: not-allowed; } + +// Replace the default browser outline with custom focus indicator textarea:focus { outline: none; } - -// used to copy things to clipboard -.copy-input { - position: absolute; - left: -999px; +input:focus { + outline: none; } .list-unstyled { @@ -160,13 +54,6 @@ textarea:focus { } .page { - // padding: rem(14px) 0; - // - // @include breakpoint(md) { - // padding: rem(28px) 0; - // } - - // max-height: 100%; height: 100%; overflow-y: scroll; } @@ -179,33 +66,36 @@ button { } .text-input { - border: 1px solid $dark-light2; + border: 2px solid $border-color; padding: rem(4px) rem(12px); position: relative; border-radius: 2px; + display: block; &::placeholder { - color: $dark-light4; + color: $gray; } &:focus { - border: 1px solid $third; - } - - &:not(.text-input-transparent) { - background: $dark-light3; - } - &.text-input-transparent { - background: transparent; + border: 2px solid $third; } &.text-input-medium { padding: rem(8px) rem(12px); } + + &.text-input-stretch { + width: 100%; + } } -.learning-count { - @include font-size('medium'); +.label-full { + width: 100%; } -.learning-date { - @include font-size('regular'); + +a { + color: $link; + + &:hover { + color: $link-hover; + } } diff --git a/web/src/components/App/_theme.scss b/web/src/components/App/_theme.scss new file mode 100644 index 00000000..8d290d55 --- /dev/null +++ b/web/src/components/App/_theme.scss @@ -0,0 +1,23 @@ +// basic colors +$black: #2a2a2a; +$white: #ffffff; +$light: #f7f9fa; +$gray: #686868; +$light-gray: #f3f3f3; + +// primary colors +$first: #333745; +$second: #e7e7e7; +$third: #4d4d8b; + +// functional colors +$border-color: #d8d8d8; +$border-color-light: $light-gray; + +$link: #6f53c0; +$link-hover: darken($link, 5%); + +$danger-text: #cb2431; +$danger-background: #f8d7da; + +$light-blue: #ecf4ff; diff --git a/web/src/components/App/_variables.scss b/web/src/components/App/_variables.scss index 86995d0e..a9f5ba18 100644 --- a/web/src/components/App/_variables.scss +++ b/web/src/components/App/_variables.scss @@ -1,39 +1,12 @@ -$dark: #3d4c53; -//$dark-light: #f5f5f5; -$dark-light: #f7f9fa; -$dark-light2: #e5e5e5; -$dark-light3: #f7f7f7; -$dark-light4: #929292; -$dark-light5: #6e6e6e; -$dark-bg: #f3f3f3; -$black: #2a2a2a; -$white: #ffffff; - -$border-color: #d8d8d8; -$border-color-light: #f3f3f3; - -//$danger-text: #721c24; -$danger-text: #cb2431; -$danger-background: #f8d7da; - -$first: #333745; -$second: #e7e7e7; - -$third: #245fc5; -// $third: #6d6daa; -// $third: #4d4d8b; - -//$third: #18a0fb; -//$third: #505061; -$third-light: #ecf4ff; - $sidebar-width: 180px; $note-sidebar-width: 244px; $xl-note-sidebar-width: 320px; $footer-height: 28px; $note-header-height: 60px; -$note-sidebar-threshold: 1280px; +$note-sidebar-threshold-value: 1280; +$note-sidebar-threshold: '#{$note-sidebar-threshold-value}px'; -$link: #0061ff; -$link-hover: #0052d9; +:export { + noteSidebarThreshold: $note-sidebar-threshold-value; +} diff --git a/web/src/components/App/index.js b/web/src/components/App/index.js index d1b6e6b1..db39fae0 100644 --- a/web/src/components/App/index.js +++ b/web/src/components/App/index.js @@ -40,7 +40,7 @@ import render from '../../routes'; import { footerPaths, isDemoPath, checkBoxedLayout } from '../../libs/paths'; import SystemMessage from '../Common/SystemMessage'; -import './module.scss'; +import './App.global.scss'; import styles from './App.module.scss'; function checkIsEditor(location, prevLocation) { @@ -145,7 +145,6 @@ function App({ location, history, doGetCurrentUser, user }) { }} /> { return ; diff --git a/web/src/components/Books/BookHolder.module.scss b/web/src/components/Books/BookHolder.module.scss index 92522816..43a1d071 100644 --- a/web/src/components/Books/BookHolder.module.scss +++ b/web/src/components/Books/BookHolder.module.scss @@ -1,5 +1,5 @@ @import '../App/responsive'; -@import '../App/variables'; +@import '../App/theme'; @import '../App/rem'; .wrapper { diff --git a/web/src/components/Books/BookItem/BookItem.module.scss b/web/src/components/Books/BookItem/BookItem.module.scss index 7506a9db..b18dace3 100644 --- a/web/src/components/Books/BookItem/BookItem.module.scss +++ b/web/src/components/Books/BookItem/BookItem.module.scss @@ -1,6 +1,6 @@ @import '../../App/rem'; @import '../../App/font'; -@import '../../App/variables'; +@import '../../App/theme'; .item { position: relative; @@ -53,7 +53,7 @@ .active { .link { - background: $third-light; + background: $light-blue; color: $black; } } diff --git a/web/src/components/Books/BookItem/MobileActions.module.scss b/web/src/components/Books/BookItem/MobileActions.module.scss index dfbe834f..9818a4c6 100644 --- a/web/src/components/Books/BookItem/MobileActions.module.scss +++ b/web/src/components/Books/BookItem/MobileActions.module.scss @@ -1,5 +1,5 @@ @import '../../App/responsive'; -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/rem'; .wrapper { diff --git a/web/src/components/Books/BookItem/index.js b/web/src/components/Books/BookItem/index.js index d0c73ce5..6732089d 100644 --- a/web/src/components/Books/BookItem/index.js +++ b/web/src/components/Books/BookItem/index.js @@ -20,7 +20,7 @@ import React, { useState } from 'react'; import classnames from 'classnames'; import { Link } from 'react-router-dom'; -import { homePath } from '../../../libs/paths'; +import { getHomePath } from '../../../libs/paths'; import Actions from './Actions'; import MobileActions from './MobileActions'; @@ -49,7 +49,7 @@ export default ({ book, demo, isFocused, setFocusedOptEl, onDeleteBook }) => { }} > diff --git a/web/src/components/Books/BookList.module.scss b/web/src/components/Books/BookList.module.scss index 43a341a0..44c13203 100644 --- a/web/src/components/Books/BookList.module.scss +++ b/web/src/components/Books/BookList.module.scss @@ -1,5 +1,5 @@ @import '../App/rem'; -@import '../App/variables'; +@import '../App/theme'; .list { background-color: white; diff --git a/web/src/components/Books/Books.module.scss b/web/src/components/Books/Books.module.scss index 77d2ea37..a887aae3 100644 --- a/web/src/components/Books/Books.module.scss +++ b/web/src/components/Books/Books.module.scss @@ -1,6 +1,6 @@ @import '../App/rem'; @import '../App/responsive'; -@import '../App/variables'; +@import '../App/theme'; @import '../App/font'; .new-book-button { @@ -14,9 +14,9 @@ .count { display: none; - @include breakpoint(lg) { + @include overSidebarThreshold { @include font-size('small'); - color: $dark; + color: $black; display: inline-block; &.hidden { diff --git a/web/src/components/Books/Content.js b/web/src/components/Books/Content.js index 07ede24e..0c947093 100644 --- a/web/src/components/Books/Content.js +++ b/web/src/components/Books/Content.js @@ -27,7 +27,7 @@ import Flash from '../Common/Flash'; import Button from '../Common/Button'; import { escapesRegExp } from '../../libs/string'; -import { homePath } from '../../libs/paths'; +import { getHomePath } from '../../libs/paths'; import DeleteBookModal from './DeleteBookModal'; import { useSearchMenuKeydown, useScrollToFocused } from '../../libs/hooks/dom'; import { getOptIdxByValue } from '../../helpers/accessibility'; @@ -48,7 +48,7 @@ function filterBooks(books, searchInput) { function handleMenuKeydownSelect(demo, history) { return option => { - const destination = homePath( + const destination = getHomePath( { book: option.uuid }, @@ -154,6 +154,7 @@ function Content({ id="T-create-book-btn" type="button" kind="third" + size="normal" className={styles['create-book-button']} disabled={isFetching} onClick={() => { diff --git a/web/src/components/Books/Content.module.scss b/web/src/components/Books/Content.module.scss index cf906e52..18f44e5c 100644 --- a/web/src/components/Books/Content.module.scss +++ b/web/src/components/Books/Content.module.scss @@ -1,6 +1,6 @@ @import '../App/rem'; @import '../App/responsive'; -@import '../App/variables'; +@import '../App/theme'; .actions { margin-top: rem(12px); diff --git a/web/src/components/Books/CreateBookModal.js b/web/src/components/Books/CreateBookModal.js index 4140392a..f5e340c0 100644 --- a/web/src/components/Books/CreateBookModal.js +++ b/web/src/components/Books/CreateBookModal.js @@ -24,7 +24,7 @@ import { withRouter } from 'react-router-dom'; import Modal, { Header } from '../Common/Modal'; import * as booksOperation from '../../operations/books'; import { addBook } from '../../actions/books'; -import { homePath } from '../../libs/paths'; +import { getHomePath } from '../../libs/paths'; import Button from '../Common/Button'; import Flash from '../Common/Flash'; @@ -114,7 +114,7 @@ function CreateBookModal({ doAddBook(book); setInProgress(false); - const dest = homePath({ book: book.uuid }); + const dest = getHomePath({ book: book.uuid }); history.push(dest); }) .catch(err => { diff --git a/web/src/components/Books/CreateBookModal.module.scss b/web/src/components/Books/CreateBookModal.module.scss index 62133307..743de2c9 100644 --- a/web/src/components/Books/CreateBookModal.module.scss +++ b/web/src/components/Books/CreateBookModal.module.scss @@ -1,6 +1,6 @@ @import '../App/rem'; @import '../App/responsive'; -@import '../App/variables'; +@import '../App/theme'; .input { margin-top: rem(8px); diff --git a/web/src/components/Books/DeleteBookModal.module.scss b/web/src/components/Books/DeleteBookModal.module.scss index 1cb08b85..fd0bce41 100644 --- a/web/src/components/Books/DeleteBookModal.module.scss +++ b/web/src/components/Books/DeleteBookModal.module.scss @@ -1,6 +1,6 @@ @import '../App/rem'; @import '../App/responsive'; -@import '../App/variables'; +@import '../App/theme'; .input { margin-top: rem(8px); diff --git a/web/src/components/Books/EmptyList.module.scss b/web/src/components/Books/EmptyList.module.scss index 9615562f..ed2d3259 100644 --- a/web/src/components/Books/EmptyList.module.scss +++ b/web/src/components/Books/EmptyList.module.scss @@ -1,10 +1,10 @@ @import '../App/rem'; @import '../App/font'; -@import '../App/variables'; +@import '../App/theme'; .wrapper { text-align: center; padding: rem(48px) 0; @include font-size('medium'); - color: $dark-light5; + color: $gray; } diff --git a/web/src/components/Books/index.js b/web/src/components/Books/index.js index 6e4aec7d..16cb9c97 100644 --- a/web/src/components/Books/index.js +++ b/web/src/components/Books/index.js @@ -118,7 +118,7 @@ function Books({ demo, doGetBooks, userData, booksData }) { /> -
+
@@ -61,4 +68,8 @@ function Button({ ); } +Button.defaultProps = { + size: 'normal' +}; + export default Button; diff --git a/web/src/components/Common/Modal/ModalBody.module.scss b/web/src/components/Common/Modal/ModalBody.module.scss index ad1e9c2e..5ffcdc8a 100644 --- a/web/src/components/Common/Modal/ModalBody.module.scss +++ b/web/src/components/Common/Modal/ModalBody.module.scss @@ -1,5 +1,5 @@ @import '../../App/responsive'; -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/rem'; @import '../../App/font'; diff --git a/web/src/components/Common/Modal/ModalHeader.module.scss b/web/src/components/Common/Modal/ModalHeader.module.scss index a0fdce80..ab37802a 100644 --- a/web/src/components/Common/Modal/ModalHeader.module.scss +++ b/web/src/components/Common/Modal/ModalHeader.module.scss @@ -1,5 +1,5 @@ @import '../../App/responsive'; -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/rem'; @import '../../App/font'; @@ -8,7 +8,7 @@ justify-content: space-between; align-items: center; width: 100%; - background: $dark-light; + background: $light; height: rem(48px); padding-left: rem(16px); border-top-right-radius: 2px; diff --git a/web/src/components/Common/Note/Note.module.scss b/web/src/components/Common/Note/Note.module.scss index aa5fa632..77f8c52e 100644 --- a/web/src/components/Common/Note/Note.module.scss +++ b/web/src/components/Common/Note/Note.module.scss @@ -1,5 +1,5 @@ @import '../../App/responsive'; -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/rem'; @import '../../App/font'; diff --git a/web/src/components/Common/Note/Placeholder.module.scss b/web/src/components/Common/Note/Placeholder.module.scss index eecbed98..9dde4d69 100644 --- a/web/src/components/Common/Note/Placeholder.module.scss +++ b/web/src/components/Common/Note/Placeholder.module.scss @@ -1,5 +1,5 @@ @import '../../App/responsive'; -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/rem'; @import '../../App/font'; diff --git a/web/src/components/Common/Page/Body.module.scss b/web/src/components/Common/Page/Body.module.scss index 4087884b..2f590bc6 100644 --- a/web/src/components/Common/Page/Body.module.scss +++ b/web/src/components/Common/Page/Body.module.scss @@ -1,6 +1,6 @@ @import '../../App/rem'; @import '../../App/responsive'; -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/font'; .wrapper { diff --git a/web/src/components/Common/Page/Header.js b/web/src/components/Common/Page/Header.js index 1864f0cf..c1203da4 100644 --- a/web/src/components/Common/Page/Header.js +++ b/web/src/components/Common/Page/Header.js @@ -27,7 +27,7 @@ import styles from './Header.module.scss'; function Header({ doToggleSidebar, heading, leftContent, rightContent }) { return (
-
+
diff --git a/web/src/components/Common/Page/Header.module.scss b/web/src/components/Common/Page/Header.module.scss index 023f0e39..f9b1d7e3 100644 --- a/web/src/components/Common/Page/Header.module.scss +++ b/web/src/components/Common/Page/Header.module.scss @@ -1,13 +1,13 @@ @import '../../App/rem'; @import '../../App/responsive'; -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/font'; .header { - background: $dark-light3; + background: $light-gray; border-bottom: 1px solid $border-color; - @include breakpoint(lg) { + @include overSidebarThreshold { background: none; padding: rem(44px) 0 0; border-bottom: none; @@ -21,7 +21,7 @@ height: rem(52px); justify-content: space-between; - @include breakpoint(lg) { + @include overSidebarThreshold { height: auto; } } @@ -42,7 +42,7 @@ @include font-size('large'); font-weight: 400; - @include breakpoint(lg) { + @include overSidebarThreshold { margin-top: 0; margin-left: 0; } diff --git a/web/src/components/Common/Popover/Popover.module.scss b/web/src/components/Common/Popover/Popover.module.scss index d3a0f8eb..9c1b8f3a 100644 --- a/web/src/components/Common/Popover/Popover.module.scss +++ b/web/src/components/Common/Popover/Popover.module.scss @@ -1,5 +1,5 @@ @import '../../App/font'; -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/responsive'; @import '../../App/rem'; diff --git a/web/src/components/Common/Popover/PopoverContent.module.scss b/web/src/components/Common/Popover/PopoverContent.module.scss index 6d760e51..a7b837f4 100644 --- a/web/src/components/Common/Popover/PopoverContent.module.scss +++ b/web/src/components/Common/Popover/PopoverContent.module.scss @@ -1,5 +1,5 @@ @import '../../App/font'; -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/responsive'; @import '../../App/rem'; diff --git a/web/src/components/Common/SearchInput/SearchInput.module.scss b/web/src/components/Common/SearchInput/SearchInput.module.scss index 3536823e..f830978e 100644 --- a/web/src/components/Common/SearchInput/SearchInput.module.scss +++ b/web/src/components/Common/SearchInput/SearchInput.module.scss @@ -1,11 +1,11 @@ @import '../../App/rem'; @import '../../App/responsive'; -@import '../../App/variables'; +@import '../../App/theme'; .wrapper { position: relative; overflow: hidden; - background: $dark-light3; + background: $light-gray; } .search-icon { @@ -15,7 +15,7 @@ transform: translateY(-50%); path { - fill: $dark-light4; + fill: $gray; } } diff --git a/web/src/components/Common/SearchableMenu/General.module.scss b/web/src/components/Common/SearchableMenu/General.module.scss index 482a3984..73380522 100644 --- a/web/src/components/Common/SearchableMenu/General.module.scss +++ b/web/src/components/Common/SearchableMenu/General.module.scss @@ -1,4 +1,4 @@ -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/font'; @import '../../App/rem'; @import '../../App/responsive'; @@ -42,7 +42,7 @@ } &.active { - background: $dark-light2; + background: $light; } &.focused { @@ -61,7 +61,7 @@ display: block; @include font-size('small'); font-weight: 600; - background: $dark-light; + background: $light; padding: rem(4px) rem(12px); border-top-left-radius: 3px; border-top-right-radius: 3px; @@ -72,7 +72,7 @@ padding: rem(8px); //background: #e7f0ff; border-bottom: 1px solid #e6e6e6; - background: $dark-light; + background: $light; } .textbox { @include font-size('small'); diff --git a/web/src/components/Common/SearchableMenu/SearchableMenu.module.scss b/web/src/components/Common/SearchableMenu/SearchableMenu.module.scss index d383a717..29007d78 100644 --- a/web/src/components/Common/SearchableMenu/SearchableMenu.module.scss +++ b/web/src/components/Common/SearchableMenu/SearchableMenu.module.scss @@ -1,4 +1,4 @@ -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/rem'; .content { diff --git a/web/src/components/Common/Sidebar/MainSidebar/MainSidebar.js b/web/src/components/Common/Sidebar/MainSidebar/MainSidebar.js index 5bec086a..4ca95e75 100644 --- a/web/src/components/Common/Sidebar/MainSidebar/MainSidebar.js +++ b/web/src/components/Common/Sidebar/MainSidebar/MainSidebar.js @@ -25,11 +25,11 @@ import SafeNavLink from '../../Link/SafeNavLink'; import SafeLink from '../../Link/SafeLink'; import { - homePath, - booksPath, - notePath, - digestsPath, - subscriptionsPath + getHomePath, + getBooksPath, + getNotePath, + getDigestsPath, + getSubscriptionPath } from '../../../../libs/paths'; import { parseSearchString } from '../../../../libs/url'; import { @@ -130,7 +130,7 @@ async function handleCreateNote({ doAddNote(note, year, month); const queryObj = parseSearchString(location.search); - const dest = notePath(note.uuid, queryObj, { isEditor: true }); + const dest = getNotePath(note.uuid, queryObj, { isEditor: true }); history.push(dest); } catch (e) { console.log('err', e); @@ -180,9 +180,9 @@ const Sidebar = ({ return () => null; }, [layoutData.sidebar, doCloseSidebar]); - const pathHome = homePath({}, { demo }); - const pathBooks = booksPath({ demo }); - const pathDigests = digestsPath({ demo }); + const pathHome = getHomePath({}, { demo }); + const pathBooks = getBooksPath({ demo }); + const pathDigests = getDigestsPath({ demo }); const user = userData.data; @@ -220,7 +220,7 @@ const Sidebar = ({ id="T-create-note-btn" type="button" className={classnames( - 'button button-slim button-stretch button-third' + 'button button-normal button-slim button-stretch button-third' )} onClick={() => { handleCreateNote({ @@ -297,7 +297,7 @@ const Sidebar = ({ })} > diff --git a/web/src/components/Common/Sidebar/MainSidebar/MainSidebar.module.scss b/web/src/components/Common/Sidebar/MainSidebar/MainSidebar.module.scss index adb1aa31..21c9ac39 100644 --- a/web/src/components/Common/Sidebar/MainSidebar/MainSidebar.module.scss +++ b/web/src/components/Common/Sidebar/MainSidebar/MainSidebar.module.scss @@ -1,4 +1,4 @@ -@import '../../../App/variables'; +@import '../../../App/theme'; @import '../../../App/responsive'; @import '../../../App/rem'; @import '../../../App/font'; diff --git a/web/src/components/Common/Sidebar/SettingsSidebar/SettingsSidebar.module.scss b/web/src/components/Common/Sidebar/SettingsSidebar/SettingsSidebar.module.scss index 0ce815ca..91ff9b29 100644 --- a/web/src/components/Common/Sidebar/SettingsSidebar/SettingsSidebar.module.scss +++ b/web/src/components/Common/Sidebar/SettingsSidebar/SettingsSidebar.module.scss @@ -1,4 +1,4 @@ -@import '../../../App/variables'; +@import '../../../App/theme'; @import '../../../App/responsive'; @import '../../../App/rem'; @import '../../../App/font'; @@ -38,6 +38,6 @@ a.back-button { display: block; margin-top: rem(20px); padding-left: rem(12px); - color: $dark-light5; + color: $gray; font-weight: 600; } diff --git a/web/src/components/Common/Sidebar/SettingsSidebar/index.js b/web/src/components/Common/Sidebar/SettingsSidebar/index.js index a3b20392..e620f117 100644 --- a/web/src/components/Common/Sidebar/SettingsSidebar/index.js +++ b/web/src/components/Common/Sidebar/SettingsSidebar/index.js @@ -21,7 +21,7 @@ import classnames from 'classnames'; import { withRouter, Link, NavLink } from 'react-router-dom'; import { connect } from 'react-redux'; -import { homePath, settingsPath } from '../../../../libs/paths'; +import { getHomePath, getSettingsPath } from '../../../../libs/paths'; import { getWindowWidth, noteSidebarThreshold, @@ -94,10 +94,10 @@ const SettingsSidebar = ({ location, layoutData, doCloseSidebar }) => {
@@ -119,13 +119,13 @@ const SettingsSidebar = ({ location, layoutData, doCloseSidebar }) => { { - return location.pathname === settingsPath('account'); + return location.pathname === getSettingsPath('account'); }} > {/* @@ -138,13 +138,15 @@ const SettingsSidebar = ({ location, layoutData, doCloseSidebar }) => { { - return location.pathname === settingsPath('notification'); + return ( + location.pathname === getSettingsPath('notification') + ); }} > {/* @@ -159,13 +161,13 @@ const SettingsSidebar = ({ location, layoutData, doCloseSidebar }) => { { - return location.pathname === settingsPath('billing'); + return location.pathname === getSettingsPath('billing'); }} > {/* diff --git a/web/src/components/Common/Sidebar/Sidebar.module.scss b/web/src/components/Common/Sidebar/Sidebar.module.scss index 027988dd..3f7ab06e 100644 --- a/web/src/components/Common/Sidebar/Sidebar.module.scss +++ b/web/src/components/Common/Sidebar/Sidebar.module.scss @@ -1,3 +1,4 @@ +@import '../../App/theme'; @import '../../App/variables'; @import '../../App/responsive'; @import '../../App/rem'; @@ -22,7 +23,7 @@ bottom: auto; } - @include breakpoint(lg) { + @include overSidebarThreshold { background: none; position: relative; left: auto; @@ -33,7 +34,7 @@ } .sidebar { - background: $dark-light; + background: $light; min-width: $sidebar-width; width: $sidebar-width; transition: 0.25s cubic-bezier(0, 0, 0, 1); @@ -94,16 +95,16 @@ &:hover { color: $black; text-decoration: none; - background: #eaeaea; + background: darken($light, 5%); } &:focus { - background: #eaeaea; + background: darken($light, 5%); outline: none; } &.link-active { - background: $dark-light2; + background: darken($light, 5%); } } diff --git a/web/src/components/Common/SidebarToggle.module.scss b/web/src/components/Common/SidebarToggle.module.scss index 599d085d..96de3945 100644 --- a/web/src/components/Common/SidebarToggle.module.scss +++ b/web/src/components/Common/SidebarToggle.module.scss @@ -1,10 +1,11 @@ @import '../App/responsive'; +@import '../App/theme'; @import '../App/variables'; .button { padding: 0; - @media (min-width: $note-sidebar-threshold) { + @include overSidebarThreshold { display: none; } } diff --git a/web/src/components/Common/SubscriberWall.js b/web/src/components/Common/SubscriberWall.js index 213aa378..37abf271 100644 --- a/web/src/components/Common/SubscriberWall.js +++ b/web/src/components/Common/SubscriberWall.js @@ -21,7 +21,7 @@ import classnames from 'classnames'; import { Link } from 'react-router-dom'; import LockIcon from '../Icons/Lock'; -import { subscriptionsPath, homePath } from '../../libs/paths'; +import { getSubscriptionPath, getHomePath } from '../../libs/paths'; import styles from './SubscriberWall.module.scss'; @@ -33,12 +33,15 @@ function SubscriberWall({ wrapperClassName }) {
Unlock Dnote Pro to get started.
- + Get started Live demo diff --git a/web/src/components/Common/Tooltip/Tooltip.module.scss b/web/src/components/Common/Tooltip/Tooltip.module.scss index 333f372a..14b8defc 100644 --- a/web/src/components/Common/Tooltip/Tooltip.module.scss +++ b/web/src/components/Common/Tooltip/Tooltip.module.scss @@ -1,4 +1,4 @@ -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/rem'; @import '../../App/font'; diff --git a/web/src/components/Digest/Digest.module.scss b/web/src/components/Digest/Digest.module.scss index 06f56be3..7ed73edc 100644 --- a/web/src/components/Digest/Digest.module.scss +++ b/web/src/components/Digest/Digest.module.scss @@ -1,10 +1,11 @@ @import '../App/responsive'; +@import '../App/theme'; @import '../App/variables'; @import '../App/rem'; .wrapper { min-height: calc(100vh - #{$note-header-height}); - background: $dark-light3; + background: $light-gray; text-align: center; padding-bottom: rem(32px); } diff --git a/web/src/components/Digests/DigestHolder.module.scss b/web/src/components/Digests/DigestHolder.module.scss index 92522816..43a1d071 100644 --- a/web/src/components/Digests/DigestHolder.module.scss +++ b/web/src/components/Digests/DigestHolder.module.scss @@ -1,5 +1,5 @@ @import '../App/responsive'; -@import '../App/variables'; +@import '../App/theme'; @import '../App/rem'; .wrapper { diff --git a/web/src/components/Digests/DigestItem.js b/web/src/components/Digests/DigestItem.js index 3a99eaf0..b6563c7b 100644 --- a/web/src/components/Digests/DigestItem.js +++ b/web/src/components/Digests/DigestItem.js @@ -5,7 +5,7 @@ import moment from 'moment'; import { Link } from 'react-router-dom'; import styles from './DigestItem.module.scss'; -import { digestPath } from '../../libs/paths'; +import { getDigestPath } from '../../libs/paths'; function DigestItem({ digest, demo }) { const [isHovered, setIsHovered] = useState(false); @@ -23,7 +23,7 @@ function DigestItem({ digest, demo }) { setIsHovered(false); }} > - + {moment(digest.created_at).format('YYYY MMM Do')} diff --git a/web/src/components/Digests/DigestItem.module.scss b/web/src/components/Digests/DigestItem.module.scss index 0ac454c8..8210177f 100644 --- a/web/src/components/Digests/DigestItem.module.scss +++ b/web/src/components/Digests/DigestItem.module.scss @@ -1,5 +1,5 @@ @import '../App/responsive'; -@import '../App/variables'; +@import '../App/theme'; @import '../App/rem'; .wrapper { @@ -22,7 +22,7 @@ .active { .link { - background: $third-light; + background: $light-blue; color: $black; } } diff --git a/web/src/components/Digests/DigestList.module.scss b/web/src/components/Digests/DigestList.module.scss index 5bf305d5..87dcca96 100644 --- a/web/src/components/Digests/DigestList.module.scss +++ b/web/src/components/Digests/DigestList.module.scss @@ -1,5 +1,5 @@ @import '../App/responsive'; -@import '../App/variables'; +@import '../App/theme'; @import '../App/rem'; .list { diff --git a/web/src/components/Digests/index.js b/web/src/components/Digests/index.js index c4c766a9..9724368b 100644 --- a/web/src/components/Digests/index.js +++ b/web/src/components/Digests/index.js @@ -89,7 +89,7 @@ function Digests({
-
+
{demo || user.cloud ? ( ) : ( diff --git a/web/src/components/EmailPreference/EmailPreference.module.scss b/web/src/components/EmailPreference/EmailPreference.module.scss index a779a459..f23136f4 100644 --- a/web/src/components/EmailPreference/EmailPreference.module.scss +++ b/web/src/components/EmailPreference/EmailPreference.module.scss @@ -1,5 +1,5 @@ @import '../App/responsive'; -@import '../App/variables'; +@import '../App/theme'; @import '../App/rem'; @import '../App/font'; @@ -7,7 +7,7 @@ text-align: center; height: 100vh; padding: rem(52px) 0; - background: $dark-bg; + background: $light-gray; } .heading { @@ -33,7 +33,7 @@ margin-top: rem(20px); a { - color: $dark-light5; + color: $gray; } } diff --git a/web/src/components/EmailPreference/index.js b/web/src/components/EmailPreference/index.js index b18f2f3a..018958be 100644 --- a/web/src/components/EmailPreference/index.js +++ b/web/src/components/EmailPreference/index.js @@ -28,7 +28,7 @@ import Flash from '../Common/Flash'; import { parseSearchString } from '../../libs/url'; import { getEmailPreference } from '../../actions/auth'; -import { loginPath } from '../../libs/paths'; +import { getLoginPath } from '../../libs/paths'; import styles from './EmailPreference.module.scss'; @@ -69,7 +69,7 @@ function EmailPreference({ Error fetching email preference: {errorMessage}.{' '} - Please login and try again. + Please login and try again. )} diff --git a/web/src/components/Footer/AccountMenu.js b/web/src/components/Footer/AccountMenu.js index e554248a..448d6f18 100644 --- a/web/src/components/Footer/AccountMenu.js +++ b/web/src/components/Footer/AccountMenu.js @@ -23,7 +23,7 @@ import { Link } from 'react-router-dom'; import Lock from '../Icons/Lock'; import Menu from '../Common/Menu'; import { signout } from '../../services/users'; -import { settingsPath } from '../../libs/paths'; +import { getSettingsPath } from '../../libs/paths'; import styles from './AccountMenu.module.scss'; @@ -66,7 +66,7 @@ const AccountMenu = ({ triggerClassName, demo, user }) => { className={classnames(styles.link, { [styles.disabled]: demo })} - to={settingsPath('account', { demo })} + to={getSettingsPath('account', { demo })} onClick={e => { if (demo) { e.preventDefault(); diff --git a/web/src/components/Footer/AccountMenu.module.scss b/web/src/components/Footer/AccountMenu.module.scss index 2209d15f..5c8d359c 100644 --- a/web/src/components/Footer/AccountMenu.module.scss +++ b/web/src/components/Footer/AccountMenu.module.scss @@ -1,5 +1,5 @@ @import '../App/font'; -@import '../App/variables'; +@import '../App/theme'; @import '../App/rem'; @import '../App/responsive'; @@ -59,7 +59,7 @@ color: black; &:hover { - background: $dark-light3; + background: $light-gray; text-decoration: none; color: #0056b3; } @@ -70,7 +70,7 @@ } &:not(.disabled):focus { - background: $dark-light3; + background: $light-gray; color: #0056b3; outline: 1px dotted gray; } diff --git a/web/src/components/Footer/Footer.module.scss b/web/src/components/Footer/Footer.module.scss index b1023fff..1646c400 100644 --- a/web/src/components/Footer/Footer.module.scss +++ b/web/src/components/Footer/Footer.module.scss @@ -1,11 +1,12 @@ @import '../App/font'; -@import '../App/variables'; +@import '../App/theme'; @import '../App/rem'; +@import '../App/variables'; .footer { @include font-size('small'); border-top: 1px solid $border-color; - background: $dark-light; + background: $light; z-index: 4; height: $footer-height; display: flex; diff --git a/web/src/components/Footer/MainFooter.module.scss b/web/src/components/Footer/MainFooter.module.scss index 74d8611d..409f7da8 100644 --- a/web/src/components/Footer/MainFooter.module.scss +++ b/web/src/components/Footer/MainFooter.module.scss @@ -1,10 +1,10 @@ @import '../App/font'; -@import '../App/variables'; +@import '../App/theme'; @import '../App/rem'; .footer { @include font-size('small'); padding: rem(32px) 0; text-align: center; - color: $dark-light5; + color: $gray; } diff --git a/web/src/components/Header/DemoHeader.js b/web/src/components/Header/DemoHeader.js index ca44322e..4f50c812 100644 --- a/web/src/components/Header/DemoHeader.js +++ b/web/src/components/Header/DemoHeader.js @@ -21,16 +21,16 @@ import classnames from 'classnames'; import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; -import { subscriptionsPath, joinPath } from '../../libs/paths'; +import { getSubscriptionPath, getJoinPath } from '../../libs/paths'; import styles from './DemoHeader.module.scss'; function getPricingPath(user) { if (user) { - return subscriptionsPath(); + return getSubscriptionPath(); } - return joinPath({ referrer: subscriptionsPath() }); + return getJoinPath({ referrer: getSubscriptionPath() }); } function DemoHeader({ user }) { @@ -49,13 +49,19 @@ function DemoHeader({ user }) {
Get started Quit demo diff --git a/web/src/components/Header/DemoHeader.module.scss b/web/src/components/Header/DemoHeader.module.scss index 30e70d03..6640e82d 100644 --- a/web/src/components/Header/DemoHeader.module.scss +++ b/web/src/components/Header/DemoHeader.module.scss @@ -1,5 +1,5 @@ @import '../App/responsive'; -@import '../App/variables'; +@import '../App/theme'; @import '../App/rem'; @import '../App/font'; diff --git a/web/src/components/Header/NoteHeader.js b/web/src/components/Header/NoteHeader.js index 35c9f5d8..8a4aa199 100644 --- a/web/src/components/Header/NoteHeader.js +++ b/web/src/components/Header/NoteHeader.js @@ -20,21 +20,21 @@ import React from 'react'; import { Link } from 'react-router-dom'; import Logo from '../Icons/Logo'; -import { homePath } from '../../libs/paths'; +import { getHomePath } from '../../libs/paths'; import styles from './NoteHeader.module.scss'; function NoteHeader({ demo }) { return (
- + Dnote Go to Dnote diff --git a/web/src/components/Header/NoteHeader.module.scss b/web/src/components/Header/NoteHeader.module.scss index cd12332f..6c4d354c 100644 --- a/web/src/components/Header/NoteHeader.module.scss +++ b/web/src/components/Header/NoteHeader.module.scss @@ -1,10 +1,11 @@ @import '../App/responsive'; -@import '../App/variables'; +@import '../App/theme'; @import '../App/rem'; @import '../App/font'; +@import '../App/variables'; .wrapper { - background: $dark-light3; + background: $light-gray; padding: rem(12px) rem(20px); height: $note-header-height; z-index: 2; diff --git a/web/src/components/Header/SubscriptionsHeader.js b/web/src/components/Header/SubscriptionsHeader.js index 503fcb89..1ff6b525 100644 --- a/web/src/components/Header/SubscriptionsHeader.js +++ b/web/src/components/Header/SubscriptionsHeader.js @@ -21,7 +21,7 @@ import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; import Logo from '../Icons/LogoWithText'; -import { homePath } from '../../libs/paths'; +import { getHomePath } from '../../libs/paths'; import styles from './SubscriptionsHeader.module.scss'; function SubscriptionsHeader({ userData }) { @@ -30,7 +30,7 @@ function SubscriptionsHeader({ userData }) { return (
- + diff --git a/web/src/components/Header/SubscriptionsHeader.module.scss b/web/src/components/Header/SubscriptionsHeader.module.scss index 46b1b5e6..75dfd581 100644 --- a/web/src/components/Header/SubscriptionsHeader.module.scss +++ b/web/src/components/Header/SubscriptionsHeader.module.scss @@ -1,5 +1,5 @@ @import '../App/responsive'; -@import '../App/variables'; +@import '../App/theme'; @import '../App/rem'; @import '../App/font'; @@ -36,5 +36,5 @@ .email { @include font-size('regular'); - color: $dark-light5; + color: $gray; } diff --git a/web/src/components/Home/NoteGroup.module.scss b/web/src/components/Home/NoteGroup.module.scss index 0d6646f7..e567e77a 100644 --- a/web/src/components/Home/NoteGroup.module.scss +++ b/web/src/components/Home/NoteGroup.module.scss @@ -1,6 +1,6 @@ @import '../App/responsive'; @import '../App/font'; -@import '../App/variables'; +@import '../App/theme'; @import '../App/rem'; .wrapper { @@ -28,10 +28,9 @@ @include font-size('small'); display: flex; justify-content: space-between; - background: $dark; color: white; padding: rem(8px) rem(12px); - background: $dark-light; + background: $light; color: $black; // border-top: 1px solid $border-color; border-bottom: 1px solid $border-color; diff --git a/web/src/components/Home/NoteGroupList.module.scss b/web/src/components/Home/NoteGroupList.module.scss index afe0b55a..21bb62ff 100644 --- a/web/src/components/Home/NoteGroupList.module.scss +++ b/web/src/components/Home/NoteGroupList.module.scss @@ -1,6 +1,6 @@ @import '../App/responsive'; @import '../App/font'; -@import '../App/variables'; +@import '../App/theme'; @import '../App/rem'; .wrapper { @@ -20,5 +20,5 @@ .content { padding: rem(40px) rem(16px); text-align: center; - color: $dark-light5; + color: $gray; } diff --git a/web/src/components/Home/NoteHolder.module.scss b/web/src/components/Home/NoteHolder.module.scss index c4ca5bac..93648aca 100644 --- a/web/src/components/Home/NoteHolder.module.scss +++ b/web/src/components/Home/NoteHolder.module.scss @@ -1,6 +1,6 @@ @import '../App/responsive'; @import '../App/font'; -@import '../App/variables'; +@import '../App/theme'; @import '../App/rem'; .wrapper { diff --git a/web/src/components/Home/NoteItem.js b/web/src/components/Home/NoteItem.js index 0a924939..df487421 100644 --- a/web/src/components/Home/NoteItem.js +++ b/web/src/components/Home/NoteItem.js @@ -24,7 +24,7 @@ import { connect } from 'react-redux'; import SafeLink from '../Common/Link/SafeLink'; import { closeNoteSidebar } from '../../actions/ui'; -import { notePath } from '../../libs/paths'; +import { getNotePath } from '../../libs/paths'; import { nanosecToSec } from '../../helpers/time'; import { excerpt } from '../../libs/string'; import { getWindowWidth, noteSidebarThreshold } from '../../libs/ui'; @@ -104,7 +104,7 @@ function NoteItem({ > diff --git a/web/src/components/Home/NoteItem.module.scss b/web/src/components/Home/NoteItem.module.scss index efdb7beb..fbbf8b43 100644 --- a/web/src/components/Home/NoteItem.module.scss +++ b/web/src/components/Home/NoteItem.module.scss @@ -1,7 +1,7 @@ @import '../App/font'; @import '../App/responsive'; @import '../App/rem'; -@import '../App/variables'; +@import '../App/theme'; .wrapper { background: white; @@ -19,7 +19,7 @@ &:hover { text-decoration: none; - background: $third-light; + background: $light-blue; color: inherit; } } @@ -61,7 +61,7 @@ .active { .link { - background: $third-light; + background: $light-blue; } } diff --git a/web/src/components/Home/NoteSidebar/BookFilter.js b/web/src/components/Home/NoteSidebar/BookFilter.js index 259ca645..4b448ce5 100644 --- a/web/src/components/Home/NoteSidebar/BookFilter.js +++ b/web/src/components/Home/NoteSidebar/BookFilter.js @@ -25,7 +25,7 @@ import SearchInput from '../../Common/SearchInput'; import Popover from '../../Common/Popover'; import CaretIcon from '../../Icons/Caret'; import CheckIcon from '../../Icons/Check'; -import { homePath, notePath, isNotePath } from '../../../libs/paths'; +import { getHomePath, getNotePath, isNotePath } from '../../../libs/paths'; import { parseSearchString } from '../../../libs/url'; import { getFacetsFromSearchStr } from '../../../libs/facets'; @@ -77,12 +77,12 @@ function getOptionDestination({ demo, location, match, option }) { book: option.value }; - ret = notePath(params.noteUUID, newSearchObj, { + ret = getNotePath(params.noteUUID, newSearchObj, { demo, isEditor: true }); } else { - ret = homePath({ book: option.value }, { demo }); + ret = getHomePath({ book: option.value }, { demo }); } return ret; diff --git a/web/src/components/Home/NoteSidebar/BookFilter.module.scss b/web/src/components/Home/NoteSidebar/BookFilter.module.scss index a4dfdd92..44b9ab81 100644 --- a/web/src/components/Home/NoteSidebar/BookFilter.module.scss +++ b/web/src/components/Home/NoteSidebar/BookFilter.module.scss @@ -1,4 +1,4 @@ -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/font'; @import '../../App/rem'; @import '../../App/responsive'; @@ -14,7 +14,7 @@ .trigger { padding: 0; - color: $dark-light5; + color: $gray; } .button-content { diff --git a/web/src/components/Home/NoteSidebar/NoteSidebar.js b/web/src/components/Home/NoteSidebar/NoteSidebar.js index 6b18892d..41992b21 100644 --- a/web/src/components/Home/NoteSidebar/NoteSidebar.js +++ b/web/src/components/Home/NoteSidebar/NoteSidebar.js @@ -39,7 +39,7 @@ import { getCipherKey } from '../../../crypto'; import BookFilter from './BookFilter'; import { getFacetsFromSearchStr } from '../../../libs/facets'; import { usePrevious } from '../../../libs/hooks'; -import { notePath, isHomePath } from '../../../libs/paths'; +import { getNotePath, isHomePath } from '../../../libs/paths'; import SidebarToggle from '../../Common/SidebarToggle'; import SubscriberWall from '../../Common/SubscriberWall'; import { isEmptyObj } from '../../../libs/obj'; @@ -253,7 +253,7 @@ function useSelectFirstNote({ notesData, location, history, demo, user }) { const firstNote = firstItem.data; const searchObj = parseSearchString(location.search); - const dest = notePath(firstNote.uuid, searchObj, { demo, isEditor: true }); + const dest = getNotePath(firstNote.uuid, searchObj, { demo, isEditor: true }); history.replace(dest); }); diff --git a/web/src/components/Home/NoteSidebar/NoteSidebar.module.scss b/web/src/components/Home/NoteSidebar/NoteSidebar.module.scss index dc894ab0..c7b3831c 100644 --- a/web/src/components/Home/NoteSidebar/NoteSidebar.module.scss +++ b/web/src/components/Home/NoteSidebar/NoteSidebar.module.scss @@ -1,7 +1,8 @@ -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/responsive'; @import '../../App/rem'; @import '../../App/font'; +@import '../../App/variables'; .sidebar { min-width: 100%; @@ -18,7 +19,7 @@ transform: translateX(0); height: 100%; - @include breakpoint(lg) { + @include overSidebarThreshold { position: relative; transform: initial; min-width: $note-sidebar-width; @@ -86,11 +87,11 @@ margin-left: rem(12px); margin-bottom: 0; @include font-size('large'); - color: $dark-light5; + color: $gray; text-transform: uppercase; font-weight: 400; - @include breakpoint(lg) { + @include overSidebarThreshold { margin-top: 0; margin-left: 0; } diff --git a/web/src/components/Home/Workspace/BookSelector.module.scss b/web/src/components/Home/Workspace/BookSelector.module.scss index 75b6593e..df586a2e 100644 --- a/web/src/components/Home/Workspace/BookSelector.module.scss +++ b/web/src/components/Home/Workspace/BookSelector.module.scss @@ -1,4 +1,4 @@ -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/responsive'; @import '../../App/rem'; diff --git a/web/src/components/Home/Workspace/Editor.module.scss b/web/src/components/Home/Workspace/Editor.module.scss index d22f7eed..4e3cbdfa 100644 --- a/web/src/components/Home/Workspace/Editor.module.scss +++ b/web/src/components/Home/Workspace/Editor.module.scss @@ -1,6 +1,7 @@ -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/responsive'; @import '../../App/rem'; +@import '../../App/variables'; @import '../../App/font'; .wrapper { diff --git a/web/src/components/Home/Workspace/EditorPlaceholder.module.scss b/web/src/components/Home/Workspace/EditorPlaceholder.module.scss index 2e3ad91b..5a67de25 100644 --- a/web/src/components/Home/Workspace/EditorPlaceholder.module.scss +++ b/web/src/components/Home/Workspace/EditorPlaceholder.module.scss @@ -1,4 +1,4 @@ -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/responsive'; @import '../../App/rem'; diff --git a/web/src/components/Home/Workspace/NoteActions.js b/web/src/components/Home/Workspace/NoteActions.js index 8eec4e80..8fa84709 100644 --- a/web/src/components/Home/Workspace/NoteActions.js +++ b/web/src/components/Home/Workspace/NoteActions.js @@ -23,7 +23,7 @@ import { connect } from 'react-redux'; import DotsIcon from '../../Icons/Dots'; import Menu from '../../Common/Menu'; -import { homePath } from '../../../libs/paths'; +import { getHomePath } from '../../../libs/paths'; import { removeNote } from '../../../actions/notes'; import { resetNote } from '../../../actions/note'; @@ -66,7 +66,7 @@ function handleRemove({ doResetEditor(); console.log('pushing'); - history.push(homePath()); + history.push(getHomePath()); }) .catch(err => { console.log('err', err); diff --git a/web/src/components/Home/Workspace/NoteActions.module.scss b/web/src/components/Home/Workspace/NoteActions.module.scss index 3ce7bed4..6b48e40c 100644 --- a/web/src/components/Home/Workspace/NoteActions.module.scss +++ b/web/src/components/Home/Workspace/NoteActions.module.scss @@ -1,4 +1,4 @@ -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/responsive'; @import '../../App/rem'; @import '../../App/font'; @@ -16,7 +16,7 @@ display: block; &:hover { - background: $dark-light3; + background: $light-gray; } } diff --git a/web/src/components/Home/Workspace/Preview.module.scss b/web/src/components/Home/Workspace/Preview.module.scss index 668aa2fd..7f441d99 100644 --- a/web/src/components/Home/Workspace/Preview.module.scss +++ b/web/src/components/Home/Workspace/Preview.module.scss @@ -1,7 +1,8 @@ -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/responsive'; @import '../../App/rem'; @import '../../App/font'; +@import '../../App/variables'; .content { padding: rem(12px) rem(24px); diff --git a/web/src/components/Home/Workspace/Workspace.module.scss b/web/src/components/Home/Workspace/Workspace.module.scss index e9f4f12a..875e5e2e 100644 --- a/web/src/components/Home/Workspace/Workspace.module.scss +++ b/web/src/components/Home/Workspace/Workspace.module.scss @@ -1,5 +1,6 @@ -@import '../../App/variables'; +@import '../../App/theme'; @import '../../App/responsive'; +@import '../../App/variables'; @import '../../App/rem'; @import '../../App/font'; @@ -54,7 +55,7 @@ min-height: rem(52px); display: flex; - @include breakpoint(lg) { + @include overSidebarThreshold { display: none; } } @@ -64,7 +65,7 @@ display: flex; align-items: center; - @include breakpoint(lg) { + @include overSidebarThreshold { display: none; } } @@ -76,7 +77,7 @@ height: 100%; - @include breakpoint(lg) { + @include overSidebarThreshold { justify-content: space-between; } } @@ -90,7 +91,7 @@ margin-left: rem(12px); margin-bottom: 0; @include font-size('large'); - color: $dark-light5; + color: $gray; text-transform: uppercase; font-weight: 600; } @@ -98,7 +99,7 @@ .desktop-book-selector { display: none; - @include breakpoint(lg) { + @include overSidebarThreshold { display: block; } } diff --git a/web/src/components/Note/Note.module.scss b/web/src/components/Note/Note.module.scss index 12982bfe..5b2ea922 100644 --- a/web/src/components/Note/Note.module.scss +++ b/web/src/components/Note/Note.module.scss @@ -1,10 +1,10 @@ @import '../App/responsive'; @import '../App/rem'; -@import '../App/variables'; +@import '../App/theme'; .wrapper { // min-height: calc(100vh - 57px); - background: $dark-light3; + background: $light-gray; flex-grow: 1; flex-basis: 0; } diff --git a/web/src/components/Settings/Account/index.js b/web/src/components/Settings/Account/index.js index a523fedc..bb43ebed 100644 --- a/web/src/components/Settings/Account/index.js +++ b/web/src/components/Settings/Account/index.js @@ -106,7 +106,7 @@ function Account({ userState }) {
-
+
{successMsg && ( diff --git a/web/src/components/Settings/Billing/Placeholder.js b/web/src/components/Settings/Billing/Placeholder.js index 8ac949bc..3817c04c 100644 --- a/web/src/components/Settings/Billing/Placeholder.js +++ b/web/src/components/Settings/Billing/Placeholder.js @@ -24,7 +24,7 @@ import styles from './Placeholder.module.scss'; function Placeholder() { return ( -
+
diff --git a/web/src/components/Settings/Billing/Placeholder.module.scss b/web/src/components/Settings/Billing/Placeholder.module.scss index 24e7a999..9e2f2c9f 100644 --- a/web/src/components/Settings/Billing/Placeholder.module.scss +++ b/web/src/components/Settings/Billing/Placeholder.module.scss @@ -1,6 +1,6 @@ @import '../../App/rem'; @import '../../App/font'; -@import '../../App/variables'; +@import '../../App/theme'; .content1 { position: relative; diff --git a/web/src/components/Settings/Billing/PlanRow.module.scss b/web/src/components/Settings/Billing/PlanRow.module.scss index 47815c49..7752dbfe 100644 --- a/web/src/components/Settings/Billing/PlanRow.module.scss +++ b/web/src/components/Settings/Billing/PlanRow.module.scss @@ -1,6 +1,6 @@ @import '../../App/rem'; @import '../../App/font'; -@import '../../App/variables'; +@import '../../App/theme'; .wrapper { padding-top: rem(48px); @@ -25,7 +25,7 @@ .desc { @include font-size('small'); - color: $dark-light5; + color: $gray; max-width: rem(400px); } diff --git a/web/src/components/Settings/Billing/index.js b/web/src/components/Settings/Billing/index.js index 160ac634..93f22e41 100644 --- a/web/src/components/Settings/Billing/index.js +++ b/web/src/components/Settings/Billing/index.js @@ -128,7 +128,7 @@ function Content({ doGetSubscription }) { return ( -
+
{successMsg && ( @@ -220,7 +220,7 @@ function Billing({ {subscriptionData.errorMessage && ( -
+
@@ -232,7 +232,7 @@ function Billing({
)} {sourceData.errorMessage && ( -
+
diff --git a/web/src/components/Settings/Notification/index.js b/web/src/components/Settings/Notification/index.js index 52fd733f..b65e724e 100644 --- a/web/src/components/Settings/Notification/index.js +++ b/web/src/components/Settings/Notification/index.js @@ -28,7 +28,7 @@ import Flash from '../../Common/Flash'; import { getEmailPreference } from '../../../actions/auth'; import FrequencyModal from './FrequencyModal'; import SettingRow from '../SettingRow'; -import { settingsPath } from '../../../libs/paths'; +import { getSettingsPath } from '../../../libs/paths'; import settingsStyles from '../Settings.module.scss'; @@ -60,7 +60,7 @@ function Email({ emailPreferenceData, doGetEmailPreference }) {
-
+
{successMsg && (
@@ -116,9 +116,9 @@ function Email({ emailPreferenceData, doGetEmailPreference }) { digests.
diff --git a/web/src/components/Settings/SettingRow.module.scss b/web/src/components/Settings/SettingRow.module.scss index 38832185..0ae401f6 100644 --- a/web/src/components/Settings/SettingRow.module.scss +++ b/web/src/components/Settings/SettingRow.module.scss @@ -1,6 +1,6 @@ @import '../App/rem'; @import '../App/font'; -@import '../App/variables'; +@import '../App/theme'; .row { padding: rem(12px) rem(12px); @@ -30,7 +30,7 @@ .desc { margin-bottom: 0; @include font-size('small'); - color: $dark-light5; + color: $gray; } .action { display: flex; diff --git a/web/src/components/Settings/Settings.module.scss b/web/src/components/Settings/Settings.module.scss index 1dcfa35a..8fde07f5 100644 --- a/web/src/components/Settings/Settings.module.scss +++ b/web/src/components/Settings/Settings.module.scss @@ -1,6 +1,6 @@ @import '../App/rem'; @import '../App/font'; -@import '../App/variables'; +@import '../App/theme'; .section { margin-top: rem(24px); @@ -14,7 +14,7 @@ @include font-size('regular'); font-weight: 400; padding-bottom: rem(4px); - background: $dark-light; + background: $light; padding: rem(4px) rem(12px); } .section-content { diff --git a/web/src/components/Splash/Splash.module.scss b/web/src/components/Splash/Splash.module.scss index 35df693a..90f3ce78 100644 --- a/web/src/components/Splash/Splash.module.scss +++ b/web/src/components/Splash/Splash.module.scss @@ -1,7 +1,7 @@ -@import '../App/variables'; +@import '../App/theme'; .wrapper { - background: $dark-bg; + background: $light-gray; min-height: 100vh; padding: 50px 15px; box-sizing: border-box; diff --git a/web/src/components/Subscription/Checkout/CountrySelect.js b/web/src/components/Subscription/Checkout/CountrySelect.js new file mode 100644 index 00000000..55bcee0a --- /dev/null +++ b/web/src/components/Subscription/Checkout/CountrySelect.js @@ -0,0 +1,34 @@ +import React from 'react'; +import classnames from 'classnames'; + +import { countries } from '../../../libs/countries'; +import CaretIcon from '../../Icons/Caret'; + +import styles from './CountrySelect.module.scss'; + +function CountrySelect({ id, className, onChange, value }) { + return ( +
+ + + +
+ ); +} + +export default CountrySelect; diff --git a/web/src/components/Subscription/Checkout/CountrySelect.module.scss b/web/src/components/Subscription/Checkout/CountrySelect.module.scss new file mode 100644 index 00000000..0d04e838 --- /dev/null +++ b/web/src/components/Subscription/Checkout/CountrySelect.module.scss @@ -0,0 +1,33 @@ +@import '../../App/responsive'; +@import '../../App/theme'; +@import '../../App/font'; +@import '../../App/rem'; + +.wrapper { + position: relative; +} + +.select { + // match the height of the stripe element with other inputs + padding: rem(7px) rem(12px); + + border: 2px solid $border-color; + background-color: #ffffff; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + appearance: none; + outline: none; + background: transparent; + + &:focus { + border: 2px solid $third; + } +} + +.caret { + position: absolute; + right: rem(16px); + top: 50%; + transform: translateY(-50%); + z-index: -1; +} diff --git a/web/src/components/Subscription/Checkout/Form.js b/web/src/components/Subscription/Checkout/Form.js new file mode 100644 index 00000000..93394552 --- /dev/null +++ b/web/src/components/Subscription/Checkout/Form.js @@ -0,0 +1,228 @@ +import React, { useState, useRef } from 'react'; +import { withRouter } from 'react-router-dom'; +import { connect } from 'react-redux'; +import classnames from 'classnames'; +import Helmet from 'react-helmet'; +import { injectStripe, CardElement } from 'react-stripe-elements'; + +import Sidebar from './Sidebar'; +import CountrySelect from './CountrySelect'; +import Flash from '../../Common/Flash'; +import * as paymentService from '../../../services/payment'; +import { getCurrentUser } from '../../../actions/auth'; +import { updateMessage } from '../../../actions/ui'; +import { getHomePath } from '../../../libs/paths'; + +import styles from './Form.module.scss'; + +const elementStyles = { + base: { + color: '#32325D', + fontFamily: 'Source Code Pro, Consolas, Menlo, monospace', + fontSize: '16px', + fontSmoothing: 'antialiased', + + '::placeholder': { + color: '#CFD7DF' + }, + ':-webkit-autofill': { + color: '#e39f48' + } + }, + invalid: { + color: '#E25950', + + '::placeholder': { + color: '#FFCCA5' + } + } +}; + +function Form({ + stripe, + stripeLoadError, + doGetCurrentUser, + doUpdateMessage, + history +}) { + const [nameOnCard, setNameOnCard] = useState(''); + const cardElementRef = useRef(null); + const [cardElementFocused, setCardElementFocused] = useState(false); + const [cardElementLoaded, setCardElementLoaded] = useState(false); + const [billingCountry, setBillingCountry] = useState(''); + const [transacting, setTransacting] = useState(false); + const [errMessage, setErrMessage] = useState(''); + + async function handleSubmit(e) { + e.preventDefault(); + + if (!cardElementLoaded) { + return; + } + if (!nameOnCard) { + setErrMessage('Please enter the name on card'); + return; + } + if (!billingCountry) { + setErrMessage('Please enter the country'); + return; + } + + setTransacting(true); + + try { + const { source, error } = await stripe.createSource({ + type: 'card', + currency: 'usd', + owner: { + name: nameOnCard + } + }); + + if (error) { + throw error; + } + + await paymentService.createSubscription({ + source, + country: billingCountry + }); + } catch (err) { + console.log('error subscribing', err); + setTransacting(false); + setErrMessage(err.message); + return; + } + + setNameOnCard(''); + setBillingCountry(''); + cardElementRef.current.clear(); + setTransacting(false); + + await doGetCurrentUser(); + doUpdateMessage('Welcome to Dnote Pro', 'info'); + history.push(getHomePath({}, { demo: false })); + } + + return ( +
+ + Subscriptions + + + {errMessage && ( + { + setErrMessage(''); + }} + > + {errMessage} + + )} + {stripeLoadError && ( + + Failed to load stripe. {stripeLoadError} + + )} + +
+
+
+

You are almost there.

+ +
+
+ +
+ +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + +
+ +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + +
+
+
+
+ +
+ +
+
+
+ ); +} + +const mapDispatchToProps = { + doGetCurrentUser: getCurrentUser, + doUpdateMessage: updateMessage +}; + +export default injectStripe( + withRouter( + connect( + null, + mapDispatchToProps + )(Form) + ) +); diff --git a/web/src/components/Subscription/Checkout/Form.module.scss b/web/src/components/Subscription/Checkout/Form.module.scss new file mode 100644 index 00000000..ed0b9714 --- /dev/null +++ b/web/src/components/Subscription/Checkout/Form.module.scss @@ -0,0 +1,69 @@ +@import '../../App/responsive'; +@import '../../App/theme'; +@import '../../App/font'; +@import '../../App/rem'; + +.wrapper { + margin-top: rem(52px); +} + +.number { + flex-grow: 1; +} + +.cvc, +.expiry { + width: rem(100px); +} + +.flash { + margin-bottom: rem(24px); +} + +.label { + @include font-size('medium'); +} + +.input { + margin-top: rem(8px); +} + +.input-row { + &:not(:first-child) { + margin-top: rem(20px); + } +} + +.card-row { + display: flex; +} + +.heading { + @include font-size('3x-large'); + font-weight: 400; +} + +.content-wrapper { + @include breakpoint(lg) { + padding-right: rem(52px); + } +} + +.content { + margin-top: rem(32px); +} + +.card-number { + // match the height of the stripe element with other inputs + padding: rem(10.4px) rem(12px); + border: 2px solid $border-color; + + &.card-number-active { + border: 2px solid $third; + } +} + +.countries-select { + width: 100%; + display: block; +} diff --git a/web/src/components/Subscription/Checkout/Sidebar.js b/web/src/components/Subscription/Checkout/Sidebar.js new file mode 100644 index 00000000..97558590 --- /dev/null +++ b/web/src/components/Subscription/Checkout/Sidebar.js @@ -0,0 +1,64 @@ +import React from 'react'; +import classnames from 'classnames'; + +import Button from '../../Common/Button'; +import ServerIcon from '../../Icons/Server'; +import GlobeIcon from '../../Icons/Globe'; + +import styles from './Sidebar.module.scss'; + +const perks = [ + { + id: 'hosted', + icon: , + value: 'Fully hosted and managed' + }, + { + id: 'support', + icon: , + value: 'Support the Dnote community and development' + } +]; + +function Sidebar({ isReady, transacting }) { + return ( +
+
+
Pro
+ +
    + {perks.map(perk => { + return ( +
  • +
    {perk.icon}
    +
    {perk.value}
    +
  • + ); + })} +
+ +
+ $3.00 +
/ month
+
+ + +
+ +

You can cancel your plan any time.

+
+ ); +} + +export default Sidebar; diff --git a/web/src/components/Subscription/Checkout/Sidebar.module.scss b/web/src/components/Subscription/Checkout/Sidebar.module.scss new file mode 100644 index 00000000..a24ae68c --- /dev/null +++ b/web/src/components/Subscription/Checkout/Sidebar.module.scss @@ -0,0 +1,77 @@ +@import '../../App/theme'; +@import '../../App/variables'; +@import '../../App/responsive'; +@import '../../App/font'; +@import '../../App/rem'; + +.wrapper { + margin-top: rem(20px); + + @include breakpoint(lg) { + margin-top: 0; + } +} + +.header { + border: 1px solid $border-color; + border-top: rem(20px) solid $third; + padding: rem(16px) rem(32px); +} + +.plan-name { + @include font-size('x-large'); + font-weight: 600; +} + +.price { + @include font-size('3x-large'); + font-weight: 600; +} + +.interval { + @include font-size('large'); + text-transform: uppercase; + font-weight: 600; + margin-left: rem(4px); +} + +.price-wrapper { + display: flex; + align-items: baseline; + margin-top: rem(16px); +} + +.purchase-button { + margin-top: rem(20px); +} + +.assurance { + @include font-size('medium'); + font-weight: 600; + margin-top: rem(20px); + text-align: center; +} + +.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); +} diff --git a/web/src/components/Subscription/Checkout/index.js b/web/src/components/Subscription/Checkout/index.js new file mode 100644 index 00000000..f101b4e4 --- /dev/null +++ b/web/src/components/Subscription/Checkout/index.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { StripeProvider, Elements } from 'react-stripe-elements'; + +import { useScript } from '../../../libs/hooks'; +import CheckoutForm from './Form'; + +function Checkout() { + const [stripeLoaded, stripeLoadError] = useScript('https://js.stripe.com/v3'); + + const key = `${__STRIPE_PUBLIC_KEY__}`; + + let stripe = null; + if (stripeLoaded) { + stripe = window.Stripe(key); + } + + return ( + + + + + + ); +} + +export default Checkout; diff --git a/web/src/components/Subscription/FeatureItem.module.scss b/web/src/components/Subscription/FeatureItem.module.scss index c6e21ce6..9122f600 100644 --- a/web/src/components/Subscription/FeatureItem.module.scss +++ b/web/src/components/Subscription/FeatureItem.module.scss @@ -1,5 +1,5 @@ @import '../App/responsive'; -@import '../App/variables'; +@import '../App/theme'; @import '../App/rem'; @import '../App/font'; diff --git a/web/src/components/Subscription/FeatureList.js b/web/src/components/Subscription/FeatureList.js new file mode 100644 index 00000000..79501aba --- /dev/null +++ b/web/src/components/Subscription/FeatureList.js @@ -0,0 +1,17 @@ +import React from 'react'; + +import FeatureItem from './FeatureItem'; + +import styles from './FeatureList.module.scss'; + +function FeatureList({ features }) { + return ( +
    + {features.map(feature => { + return ; + })} +
+ ); +} + +export default FeatureList; diff --git a/web/src/components/Subscription/FeatureList.module.scss b/web/src/components/Subscription/FeatureList.module.scss new file mode 100644 index 00000000..247ab1c4 --- /dev/null +++ b/web/src/components/Subscription/FeatureList.module.scss @@ -0,0 +1,11 @@ +@import '../App/responsive'; +@import '../App/theme'; +@import '../App/rem'; +@import '../App/font'; + +.feature-list { + list-style: none; + margin-bottom: 0; + padding-left: 0; + padding-top: rem(12px); +} diff --git a/web/src/components/Subscription/Plan/Core.js b/web/src/components/Subscription/Plan/Core.js new file mode 100644 index 00000000..6741ba2b --- /dev/null +++ b/web/src/components/Subscription/Plan/Core.js @@ -0,0 +1,27 @@ +import React from 'react'; + +import BoxIcon from '../../Icons/Box'; +import Plan from './internal'; + +const selfHostedPerks = [ + { + id: 'own-machine', + icon: , + value: 'Host on your own machine' + } +]; + +function Core({ wrapperClassName, ctaContent, bottomContent }) { + return ( + + ); +} + +export default Core; diff --git a/web/src/components/Subscription/Plan.module.scss b/web/src/components/Subscription/Plan/Plan.module.scss similarity index 88% rename from web/src/components/Subscription/Plan.module.scss rename to web/src/components/Subscription/Plan/Plan.module.scss index e1b76336..004bf394 100644 --- a/web/src/components/Subscription/Plan.module.scss +++ b/web/src/components/Subscription/Plan/Plan.module.scss @@ -1,7 +1,7 @@ -@import '../App/responsive'; -@import '../App/variables'; -@import '../App/rem'; -@import '../App/font'; +@import '../../App/responsive'; +@import '../../App/theme'; +@import '../../App/rem'; +@import '../../App/font'; .wrapper { @include breakpoint(md) { @@ -27,7 +27,7 @@ .header { border: 1px solid $border-color; - border-top: 8px solid $dark-light5; + border-top: 8px solid $gray; padding: rem(16px) rem(32px); border-radius: rem(2px); @@ -97,3 +97,7 @@ 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.js new file mode 100644 index 00000000..436300c3 --- /dev/null +++ b/web/src/components/Subscription/Plan/Pro.js @@ -0,0 +1,34 @@ +import React from 'react'; + +import Plan from './internal'; +import ServerIcon from '../../Icons/Server'; +import GlobeIcon from '../../Icons/Globe'; + +const proPerks = [ + { + id: 'hosted', + icon: , + value: 'Fully hosted and managed' + }, + { + id: 'support', + icon: , + value: 'Support the Dnote community and development' + } +]; + +function ProPlan({ wrapperClassName, ctaContent, bottomContent }) { + return ( + + ); +} + +export default ProPlan; diff --git a/web/src/components/Subscription/Plan.js b/web/src/components/Subscription/Plan/internal.js similarity index 89% rename from web/src/components/Subscription/Plan.js rename to web/src/components/Subscription/Plan/internal.js index d393c00b..682375fe 100644 --- a/web/src/components/Subscription/Plan.js +++ b/web/src/components/Subscription/Plan/internal.js @@ -19,14 +19,12 @@ import React from 'react'; import classnames from 'classnames'; -import FeatureItem from './FeatureItem'; - import styles from './Plan.module.scss'; function Plan({ name, price, - features, + bottomContent, ctaContent, interval, perks, @@ -62,11 +60,7 @@ function Plan({
-
    - {features.map(feature => { - return ; - })} -
+ {bottomContent}
); } diff --git a/web/src/components/Subscription/Subscription.module.scss b/web/src/components/Subscription/Subscription.module.scss index 0e4e5749..d895cd40 100644 --- a/web/src/components/Subscription/Subscription.module.scss +++ b/web/src/components/Subscription/Subscription.module.scss @@ -58,10 +58,6 @@ } } -.feature-bold { - font-weight: 600; -} - .pro-plan { order: 0; @include breakpoint(md) { @@ -74,3 +70,7 @@ order: initial; } } + +.feature-bold { + font-weight: 600; +} diff --git a/web/src/components/Subscription/index.js b/web/src/components/Subscription/index.js index 3ad92d5e..ced78d1b 100644 --- a/web/src/components/Subscription/index.js +++ b/web/src/components/Subscription/index.js @@ -16,47 +16,35 @@ * along with Dnote. If not, see . */ -import React, { useState } from 'react'; +import React from 'react'; import { connect } from 'react-redux'; import Helmet from 'react-helmet'; import { Link } from 'react-router-dom'; import classnames from 'classnames'; -import Spinner from '../Icons/Spinner'; -import Plan from './Plan'; -import Button from '../Common/Button'; -import ServerIcon from '../Icons/Server'; -import GlobeIcon from '../Icons/Globe'; -import BoxIcon from '../Icons/Box'; -import Flash from '../Common/Flash'; - -import * as paymentService from '../../services/payment'; -import { getMe } from '../../services/users'; -import { homePath } from '../../libs/paths'; -import { updateMessage } from '../../actions/ui'; -import { receiveUser } from '../../actions/auth'; -import { useScript } from '../../libs/hooks'; +import ProPlan from './Plan/Pro'; +import CorePlan from './Plan/Core'; +import FeatureList from './FeatureList'; +import { getSubscriptionCheckoutPath } from '../../libs/paths'; import styles from './Subscription.module.scss'; -const selfHostedPerks = [ +const proFeatures = [ { - id: 'own-machine', - icon: , - value: 'Host on your own machine' - } -]; - -const proPerks = [ - { - id: 'hosted', - icon: , - value: 'Fully hosted and managed' + id: 'core', + label:
Everything in core
}, { - id: 'support', - icon: , - value: 'Support the Dnote community and development' + id: 'host', + label:
Hosting
+ }, + { + id: 'auto', + label:
Automatic update and migration
+ }, + { + id: 'email-support', + label:
Email support
} ]; @@ -98,123 +86,91 @@ const baseFeatures = [ label:
Forum support
} ]; - -const proFeatures = [ - { - id: 'core', - label:
Everything in core
- }, - { - id: 'host', - label:
Hosting
- }, - { - id: 'auto', - label:
Automatic update and migration
- }, - { - id: 'email-support', - label:
Email support
- } -]; - -function Subscription({ userData, history, doUpdateMessage, doReceiveUser }) { - const [openingCheckout, setOpeningCheckout] = useState(false); - const [transacting, setTransacting] = useState(false); - const [stripeLoaded, stripeLoadError] = useScript( - 'https://checkout.stripe.com/checkout.js' - ); - - function hideOverlay() { - document.body.classList.remove('no-scroll'); - setTransacting(false); - } - +function Subscription({ userData }) { const user = userData.data; - function handlePayment() { - if (user.cloud) { - return; - } - - let key; - if (__PRODUCTION__) { - key = 'pk_live_xvouPZFPDDBSIyMUSLZwkXfR'; - } else { - key = 'pk_test_5926f65DQoIilZeNOiKydfoN'; - } - - setOpeningCheckout(true); - - const handler = StripeCheckout.configure({ - key, - image: 'https://s3.amazonaws.com/dnote-asset/images/logo-circle.png', - locale: 'auto', - token: async token => { - try { - await paymentService.createSubscription({ token }); - } catch (err) { - hideOverlay(); - console.log('Payment error', err); - alert('error happened with payment', err); - - return; - } - - try { - const u = await getMe(); - doReceiveUser(u); - doUpdateMessage('Welcome to Dnote Cloud!', 'info'); - history.push(homePath({}, { demo: false })); - } catch (err) { - // gracefully handle error by simply redirecting to home - console.log('error getting user', err.message); - window.location = '/'; - } - }, - opened: () => { - document.body.classList.add('no-scroll'); - - setTransacting(true); - setOpeningCheckout(true); - }, - closed: () => { - hideOverlay(); - setOpeningCheckout(false); - } - }); - - handler.open({ - name: 'Dnote Pro', - description: 'An encrypted home for your knowledge', - amount: 300, - currency: 'usd', - panelLabel: '{{amount}} monthly' - }); - } + // function handlePayment() { + // if (user.cloud) { + // return; + // } + // + // let key; + // if (__PRODUCTION__) { + // key = 'pk_live_xvouPZFPDDBSIyMUSLZwkXfR'; + // } else { + // key = 'pk_test_5926f65DQoIilZeNOiKydfoN'; + // } + // + // const handler = StripeCheckout.configure({ + // key, + // image: 'https://s3.amazonaws.com/dnote-asset/images/logo-circle.png', + // locale: 'auto', + // token: async token => { + // try { + // await paymentService.createSubscription({ token }); + // } catch (err) { + // hideOverlay(); + // console.log('Payment error', err); + // alert('error happened with payment', err); + // + // return; + // } + // + // try { + // const u = await getMe(); + // doReceiveUser(u); + // doUpdateMessage('Welcome to Dnote Cloud!', 'info'); + // history.push(getHomePath({}, { demo: false })); + // } catch (err) { + // // gracefully handle error by simply redirecting to home + // console.log('error getting user', err.message); + // window.location = '/'; + // } + // }, + // opened: () => { + // document.body.classList.add('no-scroll'); + // + // setTransacting(true); + // setOpeningCheckout(true); + // }, + // closed: () => { + // hideOverlay(); + // setOpeningCheckout(false); + // } + // }); + // + // handler.open({ + // name: 'Dnote Pro', + // description: 'An encrypted home for your knowledge', + // amount: 300, + // currency: 'usd', + // panelLabel: '{{amount}} monthly' + // }); + // } function renderPlanCta() { if (user && user.cloud) { return ( - + Go to your notes ); } return ( - + ); } @@ -228,16 +184,6 @@ function Subscription({ userData, history, doUpdateMessage, doReceiveUser }) { /> - {transacting && ( -
- -
- )} - - {stripeLoadError && ( - Stripe failed to load {stripeLoadError} - )} -

@@ -248,32 +194,25 @@ function Subscription({ userData, history, doUpdateMessage, doReceiveUser }) {
- See source code } - wrapperClassName={styles['core-plan']} + bottomContent={} /> - } />
@@ -287,12 +226,4 @@ function mapStateToProps(state) { }; } -const mapDispatchToProps = { - doReceiveUser: receiveUser, - doUpdateMessage: updateMessage -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(Subscription); +export default connect(mapStateToProps)(Subscription); diff --git a/web/src/libs/countries.js b/web/src/libs/countries.js new file mode 100644 index 00000000..2578cf22 --- /dev/null +++ b/web/src/libs/countries.js @@ -0,0 +1,252 @@ +// ISO-3166-1 alpha-3 country codes and names +export const countries = [ + { code: 'AFG', name: 'Afghanistan' }, + { code: 'ALA', name: 'Åland Islands' }, + { code: 'ALB', name: 'Albania' }, + { code: 'DZA', name: 'Algeria' }, + { code: 'ASM', name: 'American Samoa' }, + { code: 'AND', name: 'Andorra' }, + { code: 'AGO', name: 'Angola' }, + { code: 'AIA', name: 'Anguilla' }, + { code: 'ATA', name: 'Antarctica' }, + { code: 'ATG', name: 'Antigua and Barbuda' }, + { code: 'ARG', name: 'Argentina' }, + { code: 'ARM', name: 'Armenia' }, + { code: 'ABW', name: 'Aruba' }, + { code: 'AUS', name: 'Australia' }, + { code: 'AUT', name: 'Austria' }, + { code: 'AZE', name: 'Azerbaijan' }, + { code: 'BHS', name: 'Bahamas' }, + { code: 'BHR', name: 'Bahrain' }, + { code: 'BGD', name: 'Bangladesh' }, + { code: 'BRB', name: 'Barbados' }, + { code: 'BLR', name: 'Belarus' }, + { code: 'BEL', name: 'Belgium' }, + { code: 'BLZ', name: 'Belize' }, + { code: 'BEN', name: 'Benin' }, + { code: 'BMU', name: 'Bermuda' }, + { code: 'BTN', name: 'Bhutan' }, + { code: 'BOL', name: 'Bolivia, Plurinational State of' }, + { code: 'BES', name: 'Bonaire, Sint Eustatius and Saba' }, + { code: 'BIH', name: 'Bosnia and Herzegovina' }, + { code: 'BWA', name: 'Botswana' }, + { code: 'BVT', name: 'Bouvet Island' }, + { code: 'BRA', name: 'Brazil' }, + { code: 'IOT', name: 'British Indian Ocean Territory' }, + { code: 'BRN', name: 'Brunei Darussalam' }, + { code: 'BGR', name: 'Bulgaria' }, + { code: 'BFA', name: 'Burkina Faso' }, + { code: 'BDI', name: 'Burundi' }, + { code: 'KHM', name: 'Cambodia' }, + { code: 'CMR', name: 'Cameroon' }, + { code: 'CAN', name: 'Canada' }, + { code: 'CPV', name: 'Cape Verde' }, + { code: 'CYM', name: 'Cayman Islands' }, + { code: 'CAF', name: 'Central African Republic' }, + { code: 'TCD', name: 'Chad' }, + { code: 'CHL', name: 'Chile' }, + { code: 'CHN', name: 'China' }, + { code: 'CXR', name: 'Christmas Island' }, + { code: 'CCK', name: 'Cocos (Keeling) Islands' }, + { code: 'COL', name: 'Colombia' }, + { code: 'COM', name: 'Comoros' }, + { code: 'COG', name: 'Congo' }, + { code: 'COD', name: 'Congo, the Democratic Republic of the' }, + { code: 'COK', name: 'Cook Islands' }, + { code: 'CRI', name: 'Costa Rica' }, + { code: 'CIV', name: "Côte d'Ivoire" }, + { code: 'HRV', name: 'Croatia' }, + { code: 'CUB', name: 'Cuba' }, + { code: 'CUW', name: 'Curaçao' }, + { code: 'CYP', name: 'Cyprus' }, + { code: 'CZE', name: 'Czech Republic' }, + { code: 'DNK', name: 'Denmark' }, + { code: 'DJI', name: 'Djibouti' }, + { code: 'DMA', name: 'Dominica' }, + { code: 'DOM', name: 'Dominican Republic' }, + { code: 'ECU', name: 'Ecuador' }, + { code: 'EGY', name: 'Egypt' }, + { code: 'SLV', name: 'El Salvador' }, + { code: 'GNQ', name: 'Equatorial Guinea' }, + { code: 'ERI', name: 'Eritrea' }, + { code: 'EST', name: 'Estonia' }, + { code: 'ETH', name: 'Ethiopia' }, + { code: 'FLK', name: 'Falkland Islands (Malvinas)' }, + { code: 'FRO', name: 'Faroe Islands' }, + { code: 'FJI', name: 'Fiji' }, + { code: 'FIN', name: 'Finland' }, + { code: 'FRA', name: 'France' }, + { code: 'GUF', name: 'French Guiana' }, + { code: 'PYF', name: 'French Polynesia' }, + { code: 'ATF', name: 'French Southern Territories' }, + { code: 'GAB', name: 'Gabon' }, + { code: 'GMB', name: 'Gambia' }, + { code: 'GEO', name: 'Georgia' }, + { code: 'DEU', name: 'Germany' }, + { code: 'GHA', name: 'Ghana' }, + { code: 'GIB', name: 'Gibraltar' }, + { code: 'GRC', name: 'Greece' }, + { code: 'GRL', name: 'Greenland' }, + { code: 'GRD', name: 'Grenada' }, + { code: 'GLP', name: 'Guadeloupe' }, + { code: 'GUM', name: 'Guam' }, + { code: 'GTM', name: 'Guatemala' }, + { code: 'GGY', name: 'Guernsey' }, + { code: 'GIN', name: 'Guinea' }, + { code: 'GNB', name: 'Guinea-Bissau' }, + { code: 'GUY', name: 'Guyana' }, + { code: 'HTI', name: 'Haiti' }, + { code: 'HMD', name: 'Heard Island and McDonald Islands' }, + { code: 'VAT', name: 'Holy See (Vatican City State)' }, + { code: 'HND', name: 'Honduras' }, + { code: 'HKG', name: 'Hong Kong' }, + { code: 'HUN', name: 'Hungary' }, + { code: 'ISL', name: 'Iceland' }, + { code: 'IND', name: 'India' }, + { code: 'IDN', name: 'Indonesia' }, + { code: 'IRN', name: 'Iran, Islamic Republic of' }, + { code: 'IRQ', name: 'Iraq' }, + { code: 'IRL', name: 'Ireland' }, + { code: 'IMN', name: 'Isle of Man' }, + { code: 'ISR', name: 'Israel' }, + { code: 'ITA', name: 'Italy' }, + { code: 'JAM', name: 'Jamaica' }, + { code: 'JPN', name: 'Japan' }, + { code: 'JEY', name: 'Jersey' }, + { code: 'JOR', name: 'Jordan' }, + { code: 'KAZ', name: 'Kazakhstan' }, + { code: 'KEN', name: 'Kenya' }, + { code: 'KIR', name: 'Kiribati' }, + { code: 'PRK', name: "Korea, Democratic People's Republic of" }, + { code: 'KOR', name: 'Korea, Republic of' }, + { code: 'KWT', name: 'Kuwait' }, + { code: 'KGZ', name: 'Kyrgyzstan' }, + { code: 'LAO', name: "Lao People's Democratic Republic" }, + { code: 'LVA', name: 'Latvia' }, + { code: 'LBN', name: 'Lebanon' }, + { code: 'LSO', name: 'Lesotho' }, + { code: 'LBR', name: 'Liberia' }, + { code: 'LBY', name: 'Libya' }, + { code: 'LIE', name: 'Liechtenstein' }, + { code: 'LTU', name: 'Lithuania' }, + { code: 'LUX', name: 'Luxembourg' }, + { code: 'MAC', name: 'Macao' }, + { code: 'MKD', name: 'Macedonia, the former Yugoslav Republic of' }, + { code: 'MDG', name: 'Madagascar' }, + { code: 'MWI', name: 'Malawi' }, + { code: 'MYS', name: 'Malaysia' }, + { code: 'MDV', name: 'Maldives' }, + { code: 'MLI', name: 'Mali' }, + { code: 'MLT', name: 'Malta' }, + { code: 'MHL', name: 'Marshall Islands' }, + { code: 'MTQ', name: 'Martinique' }, + { code: 'MRT', name: 'Mauritania' }, + { code: 'MUS', name: 'Mauritius' }, + { code: 'MYT', name: 'Mayotte' }, + { code: 'MEX', name: 'Mexico' }, + { code: 'FSM', name: 'Micronesia, Federated States of' }, + { code: 'MDA', name: 'Moldova, Republic of' }, + { code: 'MCO', name: 'Monaco' }, + { code: 'MNG', name: 'Mongolia' }, + { code: 'MNE', name: 'Montenegro' }, + { code: 'MSR', name: 'Montserrat' }, + { code: 'MAR', name: 'Morocco' }, + { code: 'MOZ', name: 'Mozambique' }, + { code: 'MMR', name: 'Myanmar' }, + { code: 'NAM', name: 'Namibia' }, + { code: 'NRU', name: 'Nauru' }, + { code: 'NPL', name: 'Nepal' }, + { code: 'NLD', name: 'Netherlands' }, + { code: 'NCL', name: 'New Caledonia' }, + { code: 'NZL', name: 'New Zealand' }, + { code: 'NIC', name: 'Nicaragua' }, + { code: 'NER', name: 'Niger' }, + { code: 'NGA', name: 'Nigeria' }, + { code: 'NIU', name: 'Niue' }, + { code: 'NFK', name: 'Norfolk Island' }, + { code: 'MNP', name: 'Northern Mariana Islands' }, + { code: 'NOR', name: 'Norway' }, + { code: 'OMN', name: 'Oman' }, + { code: 'PAK', name: 'Pakistan' }, + { code: 'PLW', name: 'Palau' }, + { code: 'PSE', name: 'Palestinian Territory, Occupied' }, + { code: 'PAN', name: 'Panama' }, + { code: 'PNG', name: 'Papua New Guinea' }, + { code: 'PRY', name: 'Paraguay' }, + { code: 'PER', name: 'Peru' }, + { code: 'PHL', name: 'Philippines' }, + { code: 'PCN', name: 'Pitcairn' }, + { code: 'POL', name: 'Poland' }, + { code: 'PRT', name: 'Portugal' }, + { code: 'PRI', name: 'Puerto Rico' }, + { code: 'QAT', name: 'Qatar' }, + { code: 'REU', name: 'Réunion' }, + { code: 'ROU', name: 'Romania' }, + { code: 'RUS', name: 'Russian Federation' }, + { code: 'RWA', name: 'Rwanda' }, + { code: 'BLM', name: 'Saint Barthélemy' }, + { code: 'SHN', name: 'Saint Helena, Ascension and Tristan da Cunha' }, + { code: 'KNA', name: 'Saint Kitts and Nevis' }, + { code: 'LCA', name: 'Saint Lucia' }, + { code: 'MAF', name: 'Saint Martin (French part)' }, + { code: 'SPM', name: 'Saint Pierre and Miquelon' }, + { code: 'VCT', name: 'Saint Vincent and the Grenadines' }, + { code: 'WSM', name: 'Samoa' }, + { code: 'SMR', name: 'San Marino' }, + { code: 'STP', name: 'Sao Tome and Principe' }, + { code: 'SAU', name: 'Saudi Arabia' }, + { code: 'SEN', name: 'Senegal' }, + { code: 'SRB', name: 'Serbia' }, + { code: 'SYC', name: 'Seychelles' }, + { code: 'SLE', name: 'Sierra Leone' }, + { code: 'SGP', name: 'Singapore' }, + { code: 'SXM', name: 'Sint Maarten (Dutch part)' }, + { code: 'SVK', name: 'Slovakia' }, + { code: 'SVN', name: 'Slovenia' }, + { code: 'SLB', name: 'Solomon Islands' }, + { code: 'SOM', name: 'Somalia' }, + { code: 'ZAF', name: 'South Africa' }, + { code: 'SGS', name: 'South Georgia and the South Sandwich Islands' }, + { code: 'SSD', name: 'South Sudan' }, + { code: 'ESP', name: 'Spain' }, + { code: 'LKA', name: 'Sri Lanka' }, + { code: 'SDN', name: 'Sudan' }, + { code: 'SUR', name: 'Suriname' }, + { code: 'SJM', name: 'Svalbard and Jan Mayen' }, + { code: 'SWZ', name: 'Swaziland' }, + { code: 'SWE', name: 'Sweden' }, + { code: 'CHE', name: 'Switzerland' }, + { code: 'SYR', name: 'Syrian Arab Republic' }, + { code: 'TWN', name: 'Taiwan, Province of China' }, + { code: 'TJK', name: 'Tajikistan' }, + { code: 'TZA', name: 'Tanzania, United Republic of' }, + { code: 'THA', name: 'Thailand' }, + { code: 'TLS', name: 'Timor-Leste' }, + { code: 'TGO', name: 'Togo' }, + { code: 'TKL', name: 'Tokelau' }, + { code: 'TON', name: 'Tonga' }, + { code: 'TTO', name: 'Trinidad and Tobago' }, + { code: 'TUN', name: 'Tunisia' }, + { code: 'TUR', name: 'Turkey' }, + { code: 'TKM', name: 'Turkmenistan' }, + { code: 'TCA', name: 'Turks and Caicos Islands' }, + { code: 'TUV', name: 'Tuvalu' }, + { code: 'UGA', name: 'Uganda' }, + { code: 'UKR', name: 'Ukraine' }, + { code: 'ARE', name: 'United Arab Emirates' }, + { code: 'GBR', name: 'United Kingdom' }, + { code: 'USA', name: 'United States' }, + { code: 'UMI', name: 'United States Minor Outlying Islands' }, + { code: 'URY', name: 'Uruguay' }, + { code: 'UZB', name: 'Uzbekistan' }, + { code: 'VUT', name: 'Vanuatu' }, + { code: 'VEN', name: 'Venezuela, Bolivarian Republic of' }, + { code: 'VNM', name: 'Viet Nam' }, + { code: 'VGB', name: 'Virgin Islands, British' }, + { code: 'VIR', name: 'Virgin Islands, U.S.' }, + { code: 'WLF', name: 'Wallis and Futuna' }, + { code: 'ESH', name: 'Western Sahara' }, + { code: 'YEM', name: 'Yemen' }, + { code: 'ZMB', name: 'Zambia' }, + { code: 'ZWE', name: 'Zimbabwe' } +]; diff --git a/web/src/libs/paths.js b/web/src/libs/paths.js index caf32003..3292e73a 100644 --- a/web/src/libs/paths.js +++ b/web/src/libs/paths.js @@ -52,7 +52,7 @@ function getPathObj({ pathname, searchObj, state }) { return ret; } -export function homePath(searchObj = {}, options = { demo: false }) { +export function getHomePath(searchObj = {}, options = { demo: false }) { const { demo } = options; let basePath; @@ -65,7 +65,7 @@ export function homePath(searchObj = {}, options = { demo: false }) { return getPathObj({ pathname: basePath, searchObj }); } -export function booksPath(options = { demo: false }) { +export function getBooksPath(options = { demo: false }) { const { demo } = options; let basePath; @@ -78,7 +78,7 @@ export function booksPath(options = { demo: false }) { return getPathObj({ pathname: basePath }); } -export function digestsPath(options = { demo: false }) { +export function getDigestsPath(options = { demo: false }) { const { demo } = options; let basePath; @@ -91,7 +91,7 @@ export function digestsPath(options = { demo: false }) { return getPathObj({ pathname: basePath }); } -export function digestPath(digestUUID, options = { demo: false }) { +export function getDigestPath(digestUUID, options = { demo: false }) { const { demo } = options; let basePath; @@ -105,7 +105,7 @@ export function digestPath(digestUUID, options = { demo: false }) { return getPathObj({ pathname: path }); } -export function notePath(noteUUID, searchObj, { demo, isEditor }) { +export function getNotePath(noteUUID, searchObj, { demo, isEditor }) { const basePath = `/notes/${noteUUID}`; let path; @@ -122,27 +122,23 @@ export function notePath(noteUUID, searchObj, { demo, isEditor }) { }); } -export function clientsPath(client) { - if (client) { - return `/apps/${client}`; - } - - return '/apps'; -} - -export function joinPath(searchObj) { +export function getJoinPath(searchObj) { return getPathObj({ pathname: '/join', searchObj }); } -export function loginPath(searchObj) { +export function getLoginPath(searchObj) { return getPathObj({ pathname: '/login', searchObj }); } -export function subscriptionsPath(searchObj) { +export function getSubscriptionPath(searchObj) { return getPathObj({ pathname: '/subscriptions', searchObj }); } -export function settingsPath(section) { +export function getSubscriptionCheckoutPath(searchObj) { + return getPathObj({ pathname: '/subscriptions/checkout', searchObj }); +} + +export function getSettingsPath(section) { return `/settings/${section}`; } @@ -196,6 +192,16 @@ export function isSubscriptionsPath(pathname) { return Boolean(match); } +// isSubscriptionsCheckoutPath checks if the given pathname is for the subscriptions path +export function isSubscriptionsCheckoutPath(pathname) { + const match = matchPath(pathname, { + path: '/subscriptions/checkout', + exact: true + }); + + return Boolean(match); +} + // isDigestPath checks if the given pathname is for the digest path export function isDigestPath(pathname) { const match = matchPath(pathname, { @@ -274,6 +280,9 @@ export function checkBoxedLayout(location, isEditor) { if (isDigestPath(pathname)) { return false; } + if (isSubscriptionsCheckoutPath(pathname)) { + return false; + } return !isSubscriptionsPath(pathname); } diff --git a/web/src/libs/ui.js b/web/src/libs/ui.js index 10a62786..78d45d4f 100644 --- a/web/src/libs/ui.js +++ b/web/src/libs/ui.js @@ -16,8 +16,10 @@ * along with Dnote. If not, see . */ -export const noteSidebarThreshold = 1280; +import { noteSidebarThreshold } from '../components/App/_variables.scss'; + export const sidebarOverlayThreshold = 1024; +export { noteSidebarThreshold }; export function getWindowWidth() { return window.innerWidth || document.body.clientWidth; diff --git a/web/src/routes.js b/web/src/routes.js index e0a911a7..7598a106 100644 --- a/web/src/routes.js +++ b/web/src/routes.js @@ -21,7 +21,7 @@ import { renderRoutes } from 'react-router-config'; import userOnly from './hocs/userOnly'; import guestOnly from './hocs/guestOnly'; -import { joinPath, isNotePath, isDemoPath } from './libs/paths'; +import { getJoinPath, isNotePath, isDemoPath } from './libs/paths'; // Components import Home from './components/Home'; @@ -36,18 +36,22 @@ import EmailPreference from './components/EmailPreference'; import Note from './components/Note'; import Digest from './components/Digest'; import Subscription from './components/Subscription'; +import Checkout from './components/Subscription/Checkout'; import LegacyLogin from './components/LegacyLogin'; import LegacyJoin from './components/LegacyJoin'; import LegacyEncrypt from './components/LegacyEncrypt'; +const joinPath = getJoinPath().pathname; + const AuthenticatedHome = userOnly(Home); const AuthenticatedBooks = userOnly(Books); const AuthenticatedSettings = userOnly(Settings); const AuthenticatedDigest = userOnly(Digest); const AuthenticatedDigests = userOnly(Digests); const AuthenticatedNote = userOnly(Note); -const AuthenticatedSubscription = userOnly(Subscription, joinPath().pathname); +const AuthenticatedSubscription = userOnly(Subscription, joinPath); +const AuthenticatedSubscriptionCheckout = userOnly(Checkout, joinPath); const GuestLogin = guestOnly(Login); const GuestJoin = guestOnly(Join); @@ -154,6 +158,11 @@ export default function render(isEditor) { exact: true, component: AuthenticatedSubscription }, + { + path: '/subscriptions/checkout', + exact: true, + component: AuthenticatedSubscriptionCheckout + }, { path: '/legacy/login', exact: true, diff --git a/web/src/services/payment.js b/web/src/services/payment.js index 6d192684..d4478fb7 100644 --- a/web/src/services/payment.js +++ b/web/src/services/payment.js @@ -18,8 +18,13 @@ import { apiClient } from '../libs/http'; -export function createSubscription({ token }) { - return apiClient.post('/subscriptions', token); +export function createSubscription({ source, country }) { + const payload = { + source, + country + }; + + return apiClient.post('/subscriptions', payload); } export function getSubscription() { diff --git a/web/webpack/dev.config.js b/web/webpack/dev.config.js index ff48ffd7..c2de65b3 100644 --- a/web/webpack/dev.config.js +++ b/web/webpack/dev.config.js @@ -23,6 +23,7 @@ const resolve = require('./resolve'); module.exports = env => { const isStandalone = env.standalone === 'true'; + const isTest = env.isTest === 'true'; return { mode: 'development', @@ -39,6 +40,10 @@ module.exports = env => { }, module: { rules: rules({ production: false }) }, resolve, - plugins: plugins({ production: false, standalone: isStandalone }) + plugins: plugins({ + production: false, + test: isTest, + standalone: isStandalone + }) }; }; diff --git a/web/webpack/plugins.js b/web/webpack/plugins.js index 11cabba4..f6c458f6 100644 --- a/web/webpack/plugins.js +++ b/web/webpack/plugins.js @@ -20,7 +20,11 @@ const webpack = require('webpack'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const ManifestPlugin = require('webpack-manifest-plugin'); -module.exports = ({ production = false, standalone = false } = {}) => { +module.exports = ({ + production = false, + test = false, + standalone = false +} = {}) => { let domain; if (production) { domain = 'dnote.io'; @@ -42,12 +46,20 @@ module.exports = ({ production = false, standalone = false } = {}) => { baseURL = `http://${domain}:3000${basename}`; } + let stripePublicKey; + if (test) { + stripePublicKey = 'pk_live_xvouPZFPDDBSIyMUSLZwkXfR'; + } else { + stripePublicKey = 'pk_test_5926f65DQoIilZeNOiKydfoN'; + } + const compileTimeConstantForMinification = { __PRODUCTION__: production, __DEVELOPMENT__: !production, __DOMAIN__: JSON.stringify(domain), __BASE_URL__: JSON.stringify(baseURL), - __BASE_NAME__: JSON.stringify(basename) + __BASE_NAME__: JSON.stringify(basename), + __STRIPE_PUBLIC_KEY__: JSON.stringify(stripePublicKey) }; if (!production) { diff --git a/web/webpack/prod.config.js b/web/webpack/prod.config.js index df2ae6a5..950d760a 100644 --- a/web/webpack/prod.config.js +++ b/web/webpack/prod.config.js @@ -23,6 +23,7 @@ const resolve = require('./resolve'); module.exports = env => { const isStandalone = env.standalone === 'true'; + const isTest = env.isTest === 'true'; if (!isStandalone) { console.log('building a non-standalone app'); @@ -40,7 +41,11 @@ module.exports = env => { }, module: { rules: rules({ production: true }) }, resolve, - plugins: plugins({ production: true, standalone: isStandalone }), + plugins: plugins({ + production: true, + test: isTest, + standalone: isStandalone + }), optimization: { minimize: true } diff --git a/web/webpack/rules/css.js b/web/webpack/rules/css.js index ea3f4fba..39eafb82 100644 --- a/web/webpack/rules/css.js +++ b/web/webpack/rules/css.js @@ -70,12 +70,12 @@ module.exports = ({ production = false } = {}) => { return [ { - test: /\.scss$/, - exclude: /\.module\.scss$/, + test: /\.global\.scss$/, use: scssLoaders }, { - test: /\.module\.scss$/, + test: /\.scss$/, + exclude: /\.global\.scss$/, use: scssModuleLoaders } ];