Implement checkout flow (#187)

* Implement new payment flow

* Update README

* Fix style
This commit is contained in:
Sung Won Cho 2019-05-23 22:20:15 +10:00 committed by GitHub
commit 3dff87e5a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
127 changed files with 1777 additions and 720 deletions

View file

@ -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)

View file

@ -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

View file

@ -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"

73
server/Gopkg.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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)
}

View file

@ -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

View file

@ -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
}
}

View file

@ -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

View file

@ -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)
}
}

View file

@ -53,6 +53,7 @@
"__DOMAIN__": true,
"__BASE_URL__": true,
"__BASE_NAME__": true,
"__STRIPE_PUBLIC_KEY__": true,
"socket": true,
"webpackIsomorphicTools": true,
"StripeCheckout": true

66
web/package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -1,4 +1,4 @@
@import '../App/variables';
@import '../App/theme';
@import '../App/responsive';
@import '../App/rem';

View file

@ -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 {

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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 }) {
}}
/>
<Route
exact
path="/subscriptions"
render={() => {
return <MainFooter />;

View file

@ -1,5 +1,5 @@
@import '../App/responsive';
@import '../App/variables';
@import '../App/theme';
@import '../App/rem';
.wrapper {

View file

@ -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;
}
}

View file

@ -1,5 +1,5 @@
@import '../../App/responsive';
@import '../../App/variables';
@import '../../App/theme';
@import '../../App/rem';
.wrapper {

View file

@ -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 }) => {
}}
>
<Link
to={homePath({ book: book.uuid }, { demo })}
to={getHomePath({ book: book.uuid }, { demo })}
className={styles.link}
tabIndex="-1"
>

View file

@ -1,5 +1,5 @@
@import '../App/rem';
@import '../App/variables';
@import '../App/theme';
.list {
background-color: white;

View file

@ -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 {

View file

@ -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={() => {

View file

@ -1,6 +1,6 @@
@import '../App/rem';
@import '../App/responsive';
@import '../App/variables';
@import '../App/theme';
.actions {
margin-top: rem(12px);

View file

@ -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 => {

View file

@ -1,6 +1,6 @@
@import '../App/rem';
@import '../App/responsive';
@import '../App/variables';
@import '../App/theme';
.input {
margin-top: rem(8px);

View file

@ -1,6 +1,6 @@
@import '../App/rem';
@import '../App/responsive';
@import '../App/variables';
@import '../App/theme';
.input {
margin-top: rem(8px);

View file

@ -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;
}

View file

@ -118,7 +118,7 @@ function Books({ demo, doGetBooks, userData, booksData }) {
/>
<Body>
<div className="container">
<div className="container-wide">
<div className="row">
<div className="col-12">
<ContentWrapper

View file

@ -1,10 +1,10 @@
@import '../App/responsive';
@import '../App/variables';
@import '../App/theme';
@import '../App/font';
@import '../App/rem';
.page {
background: $dark-bg;
background: $light-gray;
text-align: center;
min-height: 100vh;
padding: 50px 0;

View file

@ -29,6 +29,7 @@ function Button({
id,
type,
kind,
size,
children,
className,
isBusy,
@ -40,10 +41,16 @@ function Button({
<button
id={id}
type={type}
className={classnames(className, 'button', `button-${kind}`, {
[styles.busy]: isBusy,
'button-stretch': stretch
})}
className={classnames(
className,
'button',
`button-${kind}`,
`button-${size}`,
{
[styles.busy]: isBusy,
'button-stretch': stretch
}
)}
disabled={isBusy || disabled}
onClick={onClick}
>
@ -61,4 +68,8 @@ function Button({
);
}
Button.defaultProps = {
size: 'normal'
};
export default Button;

View file

@ -1,5 +1,5 @@
@import '../../App/responsive';
@import '../../App/variables';
@import '../../App/theme';
@import '../../App/rem';
@import '../../App/font';

View file

@ -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;

View file

@ -1,5 +1,5 @@
@import '../../App/responsive';
@import '../../App/variables';
@import '../../App/theme';
@import '../../App/rem';
@import '../../App/font';

View file

@ -1,5 +1,5 @@
@import '../../App/responsive';
@import '../../App/variables';
@import '../../App/theme';
@import '../../App/rem';
@import '../../App/font';

View file

@ -1,6 +1,6 @@
@import '../../App/rem';
@import '../../App/responsive';
@import '../../App/variables';
@import '../../App/theme';
@import '../../App/font';
.wrapper {

View file

@ -27,7 +27,7 @@ import styles from './Header.module.scss';
function Header({ doToggleSidebar, heading, leftContent, rightContent }) {
return (
<div className={styles.header}>
<div className="container">
<div className="container-wide">
<div className={styles.content}>
<div className={styles.left}>
<SidebarToggle onClick={doToggleSidebar} />

View file

@ -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;
}

View file

@ -1,5 +1,5 @@
@import '../../App/font';
@import '../../App/variables';
@import '../../App/theme';
@import '../../App/responsive';
@import '../../App/rem';

View file

@ -1,5 +1,5 @@
@import '../../App/font';
@import '../../App/variables';
@import '../../App/theme';
@import '../../App/responsive';
@import '../../App/rem';

View file

@ -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;
}
}

View file

@ -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');

View file

@ -1,4 +1,4 @@
@import '../../App/variables';
@import '../../App/theme';
@import '../../App/rem';
.content {

View file

@ -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 = ({
})}
>
<SafeLink
to={subscriptionsPath()}
to={getSubscriptionPath()}
onClick={handleLinkClick}
className="button button-slim button-stretch button-first"
>

View file

@ -1,4 +1,4 @@
@import '../../../App/variables';
@import '../../../App/theme';
@import '../../../App/responsive';
@import '../../../App/rem';
@import '../../../App/font';

View file

@ -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;
}

View file

@ -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 }) => {
<div>
<div className={styles['button-wrapper']}>
<Link
to={homePath()}
to={getHomePath()}
onClick={maybeCloseSidebar}
className={classnames(
'button button-slim button-stretch button-third-outline',
'button button-normal button-slim button-stretch button-third-outline',
styles['back-button']
)}
>
@ -119,13 +119,13 @@ const SettingsSidebar = ({ location, layoutData, doCloseSidebar }) => {
<NavLink
onClick={maybeCloseSidebar}
className={classnames(styles.link, sidebarStyles.link)}
to={settingsPath('account')}
to={getSettingsPath('account')}
activeClassName={classnames(
sidebarStyles['link-active'],
styles['link-active']
)}
isActive={() => {
return location.pathname === settingsPath('account');
return location.pathname === getSettingsPath('account');
}}
>
{/* <UserIcon width="16" height="16" fill="#6e6e6e" />
@ -138,13 +138,15 @@ const SettingsSidebar = ({ location, layoutData, doCloseSidebar }) => {
<NavLink
onClick={maybeCloseSidebar}
className={classnames(styles.link, sidebarStyles.link)}
to={settingsPath('notification')}
to={getSettingsPath('notification')}
activeClassName={classnames(
sidebarStyles['link-active'],
styles['link-active']
)}
isActive={() => {
return location.pathname === settingsPath('notification');
return (
location.pathname === getSettingsPath('notification')
);
}}
>
{/* <EmailIcon width="16" height="16" fill="#6e6e6e" />
@ -159,13 +161,13 @@ const SettingsSidebar = ({ location, layoutData, doCloseSidebar }) => {
<NavLink
onClick={maybeCloseSidebar}
className={classnames(styles.link, sidebarStyles.link)}
to={settingsPath('billing')}
to={getSettingsPath('billing')}
activeClassName={classnames(
sidebarStyles['link-active'],
styles['link-active']
)}
isActive={() => {
return location.pathname === settingsPath('billing');
return location.pathname === getSettingsPath('billing');
}}
>
{/* <CreditCardIcon width="16" height="16" fill="#6e6e6e" />

View file

@ -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%);
}
}

View file

@ -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;
}
}

View file

@ -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 }) {
<div className={styles.lead}>Unlock Dnote Pro to get started.</div>
<div className={styles.actions}>
<Link to={subscriptionsPath()} className="button button-first">
<Link
to={getSubscriptionPath()}
className="button button-normal button-first"
>
Get started
</Link>
<Link
to={homePath({}, { demo: true })}
className="button button-first-outline "
to={getHomePath({}, { demo: true })}
className="button button-normal button-first-outline "
>
Live demo
</Link>

View file

@ -1,4 +1,4 @@
@import '../../App/variables';
@import '../../App/theme';
@import '../../App/rem';
@import '../../App/font';

View file

@ -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);
}

View file

@ -1,5 +1,5 @@
@import '../App/responsive';
@import '../App/variables';
@import '../App/theme';
@import '../App/rem';
.wrapper {

View file

@ -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);
}}
>
<Link className={styles.link} to={digestPath(digest.uuid, { demo })}>
<Link className={styles.link} to={getDigestPath(digest.uuid, { demo })}>
{moment(digest.created_at).format('YYYY MMM Do')}
</Link>
</li>

View file

@ -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;
}
}

View file

@ -1,5 +1,5 @@
@import '../App/responsive';
@import '../App/variables';
@import '../App/theme';
@import '../App/rem';
.list {

View file

@ -89,7 +89,7 @@ function Digests({
<Header heading="Digests" />
<Body>
<div className="container">
<div className="container-wide">
{demo || user.cloud ? (
<Content user={user} demo={demo} />
) : (

View file

@ -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;
}
}

View file

@ -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({
<Flash type="danger" wrapperClassName={styles.flash}>
Error fetching email preference: {errorMessage}.{' '}
<span>
Please <Link to={loginPath()}>login</Link> and try again.
Please <Link to={getLoginPath()}>login</Link> and try again.
</span>
</Flash>
)}

View file

@ -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();

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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 }) {
<div className={styles.right}>
<Link
to={getPricingPath(user)}
className={classnames(styles.cta, 'button button-second')}
className={classnames(
styles.cta,
'button button-normal button-second'
)}
>
Get started
</Link>
<a
href="/"
className={classnames(styles['quit-mobile'], 'button button-first')}
className={classnames(
styles['quit-mobile'],
'button button-normal button-first'
)}
>
Quit demo
</a>

View file

@ -1,5 +1,5 @@
@import '../App/responsive';
@import '../App/variables';
@import '../App/theme';
@import '../App/rem';
@import '../App/font';

View file

@ -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 (
<header className={styles.wrapper}>
<div className={styles.content}>
<Link to={homePath({}, { demo })} className={styles.brand}>
<Link to={getHomePath({}, { demo })} className={styles.brand}>
<Logo width={32} height={32} fill="#909090" className="logo" />
<span className={styles['brand-name']}>Dnote</span>
</Link>
<Link
to={homePath({}, { demo })}
className="button button-slim button-first-outline"
to={getHomePath({}, { demo })}
className="button button-normal button-slim button-first-outline"
>
Go to Dnote
</Link>

View file

@ -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;

View file

@ -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 (
<header className={styles.wrapper}>
<div className={styles.content}>
<Link to={homePath({})} className={styles.brand}>
<Link to={getHomePath({})} className={styles.brand}>
<Logo width={32} height={32} fill="black" className={styles.logo} />
</Link>

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -1,6 +1,6 @@
@import '../App/responsive';
@import '../App/font';
@import '../App/variables';
@import '../App/theme';
@import '../App/rem';
.wrapper {

View file

@ -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({
>
<SafeLink
className={styles.link}
to={notePath(note.uuid, queryObj, { demo, isEditor: true })}
to={getNotePath(note.uuid, queryObj, { demo, isEditor: true })}
draggable="false"
onClick={handleNoteItemClick}
>

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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 {

View file

@ -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);
});

View file

@ -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;
}

View file

@ -1,4 +1,4 @@
@import '../../App/variables';
@import '../../App/theme';
@import '../../App/responsive';
@import '../../App/rem';

View file

@ -1,6 +1,7 @@
@import '../../App/variables';
@import '../../App/theme';
@import '../../App/responsive';
@import '../../App/rem';
@import '../../App/variables';
@import '../../App/font';
.wrapper {

View file

@ -1,4 +1,4 @@
@import '../../App/variables';
@import '../../App/theme';
@import '../../App/responsive';
@import '../../App/rem';

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -106,7 +106,7 @@ function Account({ userState }) {
<Header heading="Account" />
<Body>
<div className="container">
<div className="container-wide">
<div className="row">
<div className="col-12 col-md-12 col-lg-10">
{successMsg && (

View file

@ -24,7 +24,7 @@ import styles from './Placeholder.module.scss';
function Placeholder() {
return (
<div className="container">
<div className="container-wide">
<div className="row">
<div className="col-12 col-md-12 col-lg-10">
<section className={settingsStyles.section}>

View file

@ -1,6 +1,6 @@
@import '../../App/rem';
@import '../../App/font';
@import '../../App/variables';
@import '../../App/theme';
.content1 {
position: relative;

View file

@ -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);
}

View file

@ -128,7 +128,7 @@ function Content({
doGetSubscription
}) {
return (
<div className="container">
<div className="container-wide">
<div className="row">
<div className="col-12 col-md-12 col-lg-10">
{successMsg && (
@ -220,7 +220,7 @@ function Billing({
<Body>
{subscriptionData.errorMessage && (
<div className="container">
<div className="container-wide">
<div className="row">
<div className="col-12 col-md-12 col-lg-10">
<Flash type="danger" wrapperClassName={settingsStyles.flash}>
@ -232,7 +232,7 @@ function Billing({
</div>
)}
{sourceData.errorMessage && (
<div className="container">
<div className="container-wide">
<div className="row">
<div className="col-12 col-md-12 col-lg-10">
<Flash type="danger" wrapperClassName={settingsStyles.flash}>

View file

@ -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 }) {
<Header heading="Notification" />
<Body>
<div className="container">
<div className="container-wide">
{successMsg && (
<div className="row">
<div className="col-12 col-lg-10">
@ -116,9 +116,9 @@ function Email({ emailPreferenceData, doGetEmailPreference }) {
digests.
</div>
<Link
to={settingsPath('account')}
to={getSettingsPath('account')}
className={classnames(
'button button-second',
'button button-normal button-second',
settingsStyles['verification-banner-cta']
)}
>

View file

@ -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;

Some files were not shown because too many files have changed in this diff Show more