mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-30 07:05:19 -05:00
Merge branch 'master' of https://github.com/sharelatex/web-sharelatex
This commit is contained in:
commit
9238462fe4
10 changed files with 1005 additions and 214 deletions
|
@ -61,11 +61,17 @@ module.exports = (app, webRouter, apiRouter)->
|
||||||
|
|
||||||
webRouter.use (req, res, next)->
|
webRouter.use (req, res, next)->
|
||||||
|
|
||||||
|
cdnBlocked = req.query.nocdn == 'true' or req.session.cdnBlocked
|
||||||
|
|
||||||
|
if cdnBlocked and !req.session.cdnBlocked?
|
||||||
|
logger.log user_id:req?.session?.user?._id, ip:req?.ip, "cdnBlocked for user, not using it and turning it off for future requets"
|
||||||
|
req.session.cdnBlocked = true
|
||||||
|
|
||||||
isDark = req.headers?.host?.slice(0,4)?.toLowerCase() == "dark"
|
isDark = req.headers?.host?.slice(0,4)?.toLowerCase() == "dark"
|
||||||
isSmoke = req.headers?.host?.slice(0,5)?.toLowerCase() == "smoke"
|
isSmoke = req.headers?.host?.slice(0,5)?.toLowerCase() == "smoke"
|
||||||
isLive = !isDark and !isSmoke
|
isLive = !isDark and !isSmoke
|
||||||
|
|
||||||
if cdnAvailable and isLive
|
if cdnAvailable and isLive and !cdnBlocked
|
||||||
staticFilesBase = Settings.cdn?.web?.host
|
staticFilesBase = Settings.cdn?.web?.host
|
||||||
else if darkCdnAvailable and isDark
|
else if darkCdnAvailable and isDark
|
||||||
staticFilesBase = Settings.cdn?.web?.darkHost
|
staticFilesBase = Settings.cdn?.web?.darkHost
|
||||||
|
|
|
@ -8,6 +8,7 @@ html(itemscope, itemtype='http://schema.org/Product')
|
||||||
window.similarproducts = true
|
window.similarproducts = true
|
||||||
style [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {display: none !important; display: none; }
|
style [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {display: none !important; display: none; }
|
||||||
|
|
||||||
|
|
||||||
-if (typeof(gaExperiments) != "undefined")
|
-if (typeof(gaExperiments) != "undefined")
|
||||||
|!{gaExperiments}
|
|!{gaExperiments}
|
||||||
|
|
||||||
|
@ -52,7 +53,15 @@ html(itemscope, itemtype='http://schema.org/Product')
|
||||||
|
|
||||||
block scripts
|
block scripts
|
||||||
script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
|
script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
|
||||||
|
script(type="text/javascript").
|
||||||
|
var noCdnKey = "nocdn=true"
|
||||||
|
var cdnBlocked = typeof jQuery === 'undefined'
|
||||||
|
var noCdnAlreadyInUrl = window.location.href.indexOf(noCdnKey) != -1 //prevent loops
|
||||||
|
if (cdnBlocked && !noCdnAlreadyInUrl) {
|
||||||
|
window.location.search += '&'+noCdnKey;
|
||||||
|
}
|
||||||
script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
|
script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
|
||||||
|
|
||||||
script.
|
script.
|
||||||
window.sharelatex = {
|
window.sharelatex = {
|
||||||
siteUrl: '#{settings.siteUrl}',
|
siteUrl: '#{settings.siteUrl}',
|
||||||
|
|
|
@ -51,226 +51,341 @@ block content
|
||||||
div(ng-if="normalPrice")
|
div(ng-if="normalPrice")
|
||||||
span.small Normally {{price.currency.symbol}}{{normalPrice}}
|
span.small Normally {{price.currency.symbol}}{{normalPrice}}
|
||||||
.row
|
.row
|
||||||
.col-md-12
|
div(sixpack-switch="subscription-form")
|
||||||
form(ng-show="planName")
|
.col-md-12(sixpack-default)
|
||||||
|
form(ng-show="planName",novalidate)
|
||||||
|
|
||||||
.row
|
|
||||||
.col-md-12
|
|
||||||
.form-group
|
|
||||||
.row
|
|
||||||
.col-md-6
|
|
||||||
label.radio-inline
|
|
||||||
input.paymentTypeOption(type="radio",value="credit_card", ng-model="paymentMethod")
|
|
||||||
i.fa.fa-cc-mastercard.fa-3x
|
|
||||||
span
|
|
||||||
i.fa.fa-cc-visa.fa-3x
|
|
||||||
.col-md-6
|
|
||||||
label.radio-inline
|
|
||||||
input.paymentTypeOption(type="radio", value="paypal", ng-model="paymentMethod")
|
|
||||||
i.fa.fa-cc-paypal.fa-3x
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.alert.alert-warning.small(ng-show="genericError")
|
|
||||||
strong {{genericError}}
|
|
||||||
|
|
||||||
span(ng-hide="paymentMethod == 'paypal'")
|
|
||||||
.row
|
.row
|
||||||
.col-md-12
|
.col-md-12
|
||||||
.form-group
|
.form-group
|
||||||
div.alert.alert-warning.small(ng-hide="validation.correctCvv") #{translate("invalid")} CVV
|
.row
|
||||||
div.alert.alert-warning.small(ng-hide="validation.correctCardNumber") #{translate("invalid")} #{translate("credit_card_number")}
|
.col-md-6
|
||||||
|
label.radio-inline
|
||||||
|
input.paymentTypeOption(type="radio",value="credit_card", ng-model="paymentMethod.value")
|
||||||
|
i.fa.fa-cc-mastercard.fa-3x
|
||||||
|
span
|
||||||
|
i.fa.fa-cc-visa.fa-3x
|
||||||
|
.col-md-6
|
||||||
|
label.radio-inline
|
||||||
|
input.paymentTypeOption(type="radio", value="paypal", ng-model="paymentMethod.value")
|
||||||
|
i.fa.fa-cc-paypal.fa-3x
|
||||||
|
|
||||||
|
.alert.alert-warning.small(ng-show="genericError")
|
||||||
|
strong {{genericError}}
|
||||||
|
|
||||||
|
span(ng-hide="paymentMethod.value == 'paypal'")
|
||||||
|
.row
|
||||||
|
.col-md-12
|
||||||
|
.form-group
|
||||||
|
div.alert.alert-warning.small(ng-hide="validation.correctCvv") #{translate("invalid")} CVV
|
||||||
|
div.alert.alert-warning.small(ng-hide="validation.correctCardNumber") #{translate("invalid")} #{translate("credit_card_number")}
|
||||||
|
.row
|
||||||
|
.col-md-6
|
||||||
|
.form-group(ng-class="validation.number == false || validation.errorFields.number ? 'has-error' : ''")
|
||||||
|
input.form-control(ng-model='data.number', ng-blur="validateCardNumber()", placeholder="#{translate('credit_card_number')}")
|
||||||
|
.col-md-3
|
||||||
|
.form-group(ng-class="validation.correctCvv == false || validation.errorFields.cvv ? 'has-error' : ''")
|
||||||
|
input.form-control(ng-model='data.cvv', ng-blur="validateCvv()", placeholder="CVV")
|
||||||
|
.row
|
||||||
|
.col-md-12
|
||||||
|
div.alert.alert-warning.small(ng-hide="validation.correctExpiry") #{translate("invalid")} #{translate("expiry")}
|
||||||
|
.row
|
||||||
|
.col-md-3
|
||||||
|
.form-group(ng-class="validation.correctExpiry == false || validation.errorFields.month ? 'has-error' : ''")
|
||||||
|
select.form-control(data-recurly='month', ng-change="validateExpiry()", ng-model='data.month')
|
||||||
|
option(value="", disabled, selected) Month
|
||||||
|
option(value="01") 01
|
||||||
|
option(value="02") 02
|
||||||
|
option(value="03") 03
|
||||||
|
option(value="04") 04
|
||||||
|
option(value="05") 05
|
||||||
|
option(value="06") 06
|
||||||
|
option(value="07") 07
|
||||||
|
option(value="08") 08
|
||||||
|
option(value="09") 09
|
||||||
|
option(value="10") 10
|
||||||
|
option(value="11") 11
|
||||||
|
option(value="12") 12
|
||||||
|
.col-md-4
|
||||||
|
.form-group(ng-class="validation.correctExpiry == false || validation.errorFields.year ? 'has-error' : ''")
|
||||||
|
select.form-control(data-recurly='year', ng-change="validateExpiry()", ng-model='data.year')
|
||||||
|
option(value="", disabled, selected) Year
|
||||||
|
option(value="2016") 2016
|
||||||
|
option(value="2017") 2017
|
||||||
|
option(value="2018") 2018
|
||||||
|
option(value="2019") 2019
|
||||||
|
option(value="2020") 2020
|
||||||
|
option(value="2021") 2021
|
||||||
|
option(value="2022") 2022
|
||||||
|
option(value="2023") 2023
|
||||||
|
option(value="2024") 2024
|
||||||
|
option(value="2025") 2025
|
||||||
|
option(value="2026") 2026
|
||||||
.row
|
.row
|
||||||
.col-md-6
|
.col-md-6
|
||||||
.form-group(ng-class="validation.number == false || validation.errorFields.number ? 'has-error' : ''")
|
.form-group(ng-class="validation.errorFields.first_name ? 'has-error' : ''")
|
||||||
input.form-control(ng-model='data.number', ng-blur="validateCardNumber()", placeholder="#{translate('credit_card_number')}")
|
input.form-control(type='text', value='', maxlength='255', , onkeyup='', data-recurly="first_name", ng-model="data.first_name", required, placeholder="#{translate('first_name')}")
|
||||||
.col-md-3
|
.col-md-6
|
||||||
.form-group(ng-class="validation.correctCvv == false || validation.errorFields.cvv ? 'has-error' : ''")
|
.form-group(ng-class="validation.errorFields.last_name ? 'has-error' : ''")
|
||||||
input.form-control(ng-model='data.cvv', ng-blur="validateCvv()", placeholder="CVV")
|
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="last_name", ng-model="data.last_name", required, placeholder="#{translate('last_name')}")
|
||||||
|
hr
|
||||||
.row
|
.row
|
||||||
.col-md-12
|
.col-md-12
|
||||||
div.alert.alert-warning.small(ng-hide="validation.correctExpiry") #{translate("invalid")} #{translate("expiry")}
|
.form-group(ng-class="validation.errorFields.address1 ? 'has-error' : ''")
|
||||||
|
label #{translate("billing_address")}
|
||||||
|
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address1", placeholder="#{translate('address')}")
|
||||||
|
.form-group(ng-class="validation.errorFields.address2 ? 'has-error' : ''")
|
||||||
|
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address2", placeholder="#{translate('address')}")
|
||||||
|
.form-group(ng-class="validation.errorFields.state ? 'has-error' : ''")
|
||||||
|
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.state", placeholder="#{translate('state')}")
|
||||||
.row
|
.row
|
||||||
.col-md-3
|
.col-md-7
|
||||||
.form-group(ng-class="validation.correctExpiry == false || validation.errorFields.month ? 'has-error' : ''")
|
.form-group(ng-class="validation.errorFields.city ? 'has-error' : ''")
|
||||||
select.form-control(data-recurly='month', ng-change="validateExpiry()", ng-model='data.month')
|
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="city", ng-model="data.city", placeholder="#{translate('city')}")
|
||||||
option(value="", disabled, selected) Month
|
.col-md-5(ng-class="validation.errorFields.postal_code ? 'has-error' : ''")
|
||||||
option(value="01") 01
|
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="postal_code", ng-model="data.postal_code", placeholder="#{translate('zip_post_code')}")
|
||||||
option(value="02") 02
|
.row
|
||||||
option(value="03") 03
|
.col-md-7
|
||||||
option(value="04") 04
|
.form-group(ng-class="validation.errorFields.country ? 'has-error' : ''")
|
||||||
option(value="05") 05
|
select.form-control(data-recurly="country", ng-model="data.country", ng-change="updateCountry()", required)
|
||||||
option(value="06") 06
|
mixin countries_options()
|
||||||
option(value="07") 07
|
.row
|
||||||
option(value="08") 08
|
.col-md-8
|
||||||
option(value="09") 09
|
if (showCouponField)
|
||||||
option(value="10") 10
|
.form-group
|
||||||
option(value="11") 11
|
input.form-control(type='text', ng-blur="applyCoupon()", ng-model="data.coupon", placeholder="#{translate('coupon')}")
|
||||||
option(value="12") 12
|
.row
|
||||||
.col-md-4
|
.col-md-8
|
||||||
.form-group(ng-class="validation.correctExpiry == false || validation.errorFields.year ? 'has-error' : ''")
|
if (showVatField)
|
||||||
select.form-control(data-recurly='year', ng-change="validateExpiry()", ng-model='data.year')
|
.form-group
|
||||||
option(value="", disabled, selected) Year
|
input.form-control(type='text', ng-blur="applyVatNumber()", ng-model="data.vat_number", placeholder="#{translate('vat_number')}")
|
||||||
option(value="2016") 2016
|
.row
|
||||||
option(value="2017") 2017
|
.col-xs-7
|
||||||
option(value="2018") 2018
|
|
||||||
option(value="2019") 2019
|
|
||||||
option(value="2020") 2020
|
|
||||||
option(value="2021") 2021
|
|
||||||
option(value="2022") 2022
|
|
||||||
option(value="2023") 2023
|
|
||||||
option(value="2024") 2024
|
|
||||||
option(value="2025") 2025
|
|
||||||
option(value="2026") 2026
|
|
||||||
.row
|
|
||||||
.col-md-6
|
|
||||||
.form-group(ng-class="validation.errorFields.first_name ? 'has-error' : ''")
|
|
||||||
input.form-control(type='text', value='', maxlength='255', , onkeyup='', data-recurly="first_name", ng-model="data.first_name", required, placeholder="#{translate('first_name')}")
|
|
||||||
.col-md-6
|
|
||||||
.form-group(ng-class="validation.errorFields.last_name ? 'has-error' : ''")
|
|
||||||
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="last_name", ng-model="data.last_name", required, placeholder="#{translate('last_name')}")
|
|
||||||
hr
|
|
||||||
.row
|
|
||||||
.col-md-12
|
|
||||||
.form-group(ng-class="validation.errorFields.address1 ? 'has-error' : ''")
|
|
||||||
label #{translate("billing_address")}
|
|
||||||
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address1", placeholder="#{translate('address')}")
|
|
||||||
.form-group(ng-class="validation.errorFields.address2 ? 'has-error' : ''")
|
|
||||||
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address2", placeholder="#{translate('address')}")
|
|
||||||
.form-group(ng-class="validation.errorFields.state ? 'has-error' : ''")
|
|
||||||
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.state", placeholder="#{translate('state')}")
|
|
||||||
.row
|
|
||||||
.col-md-7
|
|
||||||
.form-group(ng-class="validation.errorFields.city ? 'has-error' : ''")
|
|
||||||
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="city", ng-model="data.city", placeholder="#{translate('city')}")
|
|
||||||
.col-md-5(ng-class="validation.errorFields.postal_code ? 'has-error' : ''")
|
|
||||||
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="postal_code", ng-model="data.postal_code", placeholder="#{translate('zip_post_code')}")
|
|
||||||
.row
|
|
||||||
.col-md-7
|
|
||||||
.form-group(ng-class="validation.errorFields.country ? 'has-error' : ''")
|
|
||||||
select.form-control(data-recurly="country", ng-model="data.country", ng-change="updateCountry()", required)
|
|
||||||
mixin countries_options()
|
|
||||||
.row
|
|
||||||
.col-md-8
|
|
||||||
if (showCouponField)
|
|
||||||
.form-group
|
.form-group
|
||||||
input.form-control(type='text', ng-blur="applyCoupon()", ng-model="data.coupon", placeholder="#{translate('coupon')}")
|
button.btn.btn-success(ng-click="submit()", ng-disabled="processing", sixpack-convert="payment-left-menu-bottom") #{translate("upgrade_now")}
|
||||||
.row
|
|
||||||
.col-md-8
|
.col-xs-3.pricingBreakdown
|
||||||
|
div(ng-if="price.next.subtotal != price.next.total") Subtotal
|
||||||
|
div(ng-if="price.next.tax!='0.00'") Tax
|
||||||
|
div
|
||||||
|
strong Total
|
||||||
|
.col-xs-2
|
||||||
|
div(ng-if="price.next.subtotal != price.next.total").pull-right {{price.currency.symbol}}{{price.next.subtotal}}
|
||||||
|
div(ng-if="price.next.tax!='0.00'").pull-right {{price.currency.symbol}}{{price.next.tax}}
|
||||||
|
div.pull-right
|
||||||
|
strong {{price.currency.symbol}}{{price.next.total}}
|
||||||
|
|
||||||
|
.col-md-12(sixpack-when="simple")
|
||||||
|
form(
|
||||||
|
ng-if="planName"
|
||||||
|
name="simpleCCForm"
|
||||||
|
novalidate
|
||||||
|
)
|
||||||
|
div.payment-method-toggle
|
||||||
|
a.payment-method-toggle-switch(
|
||||||
|
href
|
||||||
|
ng-click="setPaymentMethod('credit_card');"
|
||||||
|
ng-class="paymentMethod.value === 'credit_card' ? 'payment-method-toggle-switch-selected' : ''"
|
||||||
|
)
|
||||||
|
i.fa.fa-cc-mastercard.fa-2x
|
||||||
|
span
|
||||||
|
i.fa.fa-cc-visa.fa-2x
|
||||||
|
span
|
||||||
|
i.fa.fa-cc-amex.fa-2x
|
||||||
|
a.payment-method-toggle-switch(
|
||||||
|
href
|
||||||
|
ng-click="setPaymentMethod('paypal');"
|
||||||
|
ng-class="paymentMethod.value === 'paypal' ? 'payment-method-toggle-switch-selected' : ''"
|
||||||
|
)
|
||||||
|
i.fa.fa-cc-paypal.fa-2x
|
||||||
|
|
||||||
|
.alert.alert-warning.small(ng-show="genericError")
|
||||||
|
strong {{genericError}}
|
||||||
|
|
||||||
|
div(ng-if="paymentMethod.value === 'credit_card'")
|
||||||
|
.row
|
||||||
|
.col-xs-6
|
||||||
|
.form-group(ng-class="validation.errorFields.first_name || inputHasError(simpleCCForm.firstName) ? 'has-error' : ''")
|
||||||
|
label(for="first-name") #{translate('first_name')}
|
||||||
|
input#first-name.form-control(
|
||||||
|
type="text"
|
||||||
|
maxlength='255'
|
||||||
|
data-recurly="first_name"
|
||||||
|
name="firstName"
|
||||||
|
ng-model="data.first_name"
|
||||||
|
required
|
||||||
|
)
|
||||||
|
span.input-feedback-message {{ simpleCCForm.firstName.$error.required ? 'This field is required' : '' }}
|
||||||
|
.col-xs-6
|
||||||
|
.form-group(for="last-name",ng-class="validation.errorFields.last_name || inputHasError(simpleCCForm.lastName)? 'has-error' : ''")
|
||||||
|
label(for="last-name") #{translate('last_name')}
|
||||||
|
input#last-name.form-control(
|
||||||
|
type="text"
|
||||||
|
maxlength='255'
|
||||||
|
data-recurly="last_name"
|
||||||
|
name="lastName"
|
||||||
|
ng-model="data.last_name"
|
||||||
|
required
|
||||||
|
)
|
||||||
|
span.input-feedback-message {{ simpleCCForm.lastName.$error.required ? 'This field is required' : '' }}
|
||||||
|
|
||||||
|
.form-group(ng-class="validation.correctCardNumber == false || validation.errorFields.number || inputHasError(simpleCCForm.ccNumber) ? 'has-error' : ''")
|
||||||
|
label(for="card-no") #{translate("credit_card_number")}
|
||||||
|
input#card-no.form-control(
|
||||||
|
type="text"
|
||||||
|
ng-model="data.number"
|
||||||
|
name="ccNumber"
|
||||||
|
ng-focus="validation.correctCardNumber = true; validation.errorFields.number = false;"
|
||||||
|
ng-blur="validateCardNumber();"
|
||||||
|
required
|
||||||
|
cc-format-card-number
|
||||||
|
)
|
||||||
|
span.input-feedback-message {{ simpleCCForm.ccNumber.$error.required ? 'This field is required' : 'Please re-check the card number' }}
|
||||||
|
|
||||||
|
.row
|
||||||
|
.col-xs-6
|
||||||
|
.form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry || inputHasError(simpleCCForm.expiry) ? 'has-error' : ''")
|
||||||
|
label #{translate("expiry")}
|
||||||
|
input.form-control(
|
||||||
|
type="text"
|
||||||
|
ng-model="data.mmYY"
|
||||||
|
name="expiry"
|
||||||
|
placeholder="MM / YY"
|
||||||
|
ng-focus="validation.correctExpiry = true; validation.errorFields.expiry = false;"
|
||||||
|
ng-blur="updateExpiry(); validateExpiry()"
|
||||||
|
required
|
||||||
|
cc-format-expiry
|
||||||
|
)
|
||||||
|
span.input-feedback-message {{ simpleCCForm.expiry.$error.required ? 'This field is required' : 'Please re-check the expiry date' }}
|
||||||
|
|
||||||
|
.col-xs-6
|
||||||
|
.form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv || inputHasError(simpleCCForm.cvv) ? 'has-error' : ''")
|
||||||
|
label #{translate("security_code")}
|
||||||
|
input.form-control(
|
||||||
|
type="text"
|
||||||
|
ng-model="data.cvv"
|
||||||
|
ng-focus="validation.correctCvv = true; validation.errorFields.cvv = false;"
|
||||||
|
ng-blur="validateCvv()"
|
||||||
|
name="cvv"
|
||||||
|
required
|
||||||
|
cc-format-sec-code
|
||||||
|
)
|
||||||
|
.form-control-feedback
|
||||||
|
a.form-helper(
|
||||||
|
href
|
||||||
|
tabindex="-1"
|
||||||
|
tooltip-template="'cvv-tooltip-tpl.html'"
|
||||||
|
tooltip-trigger="mouseenter"
|
||||||
|
tooltip-append-to-body="true"
|
||||||
|
) ?
|
||||||
|
span.input-feedback-message {{ simpleCCForm.cvv.$error.required ? 'This field is required' : 'Please re-check the security code' }}
|
||||||
|
|
||||||
|
|
||||||
|
div
|
||||||
|
.form-group(ng-class="validation.errorFields.country || inputHasError(simpleCCForm.country) ? 'has-error' : ''")
|
||||||
|
label(for="country") #{translate('country')}
|
||||||
|
select#country.form-control(
|
||||||
|
data-recurly="country"
|
||||||
|
ng-model="data.country"
|
||||||
|
name="country"
|
||||||
|
ng-change="updateCountry()"
|
||||||
|
required
|
||||||
|
)
|
||||||
|
mixin countries_options()
|
||||||
|
span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }}
|
||||||
|
|
||||||
if (showVatField)
|
if (showVatField)
|
||||||
.form-group
|
.form-group
|
||||||
input.form-control(type='text', ng-blur="applyVatNumber()", ng-model="data.vat_number", placeholder="#{translate('vat_number')}")
|
label(for="vat-no") #{translate('vat_number')}
|
||||||
.row
|
input#vat-no.form-control(
|
||||||
.col-xs-7
|
type="text"
|
||||||
.form-group
|
ng-blur="applyVatNumber()"
|
||||||
button.btn.btn-success(ng-click="submit()", ng-disabled="processing", sixpack-convert="payment-left-menu-bottom") #{translate("upgrade_now")}
|
ng-model="data.vat_number"
|
||||||
|
)
|
||||||
.col-xs-3.pricingBreakdown
|
if (showCouponField)
|
||||||
div(ng-if="price.next.subtotal != price.next.total") Subtotal
|
.form-group
|
||||||
div(ng-if="price.next.tax!='0.00'") Tax
|
label(for="coupon-code") #{translate('coupon_code')}
|
||||||
div
|
input#coupon-code.form-control(
|
||||||
strong Total
|
type="text"
|
||||||
.col-xs-2
|
ng-blur="applyCoupon()"
|
||||||
div(ng-if="price.next.subtotal != price.next.total").pull-right {{price.currency.symbol}}{{price.next.subtotal}}
|
ng-model="data.coupon"
|
||||||
div(ng-if="price.next.tax!='0.00'").pull-right {{price.currency.symbol}}{{price.next.tax}}
|
)
|
||||||
div.pull-right
|
|
||||||
strong {{price.currency.symbol}}{{price.next.total}}
|
p(ng-if="paymentMethod.value === 'paypal'") #{translate("paypal_upgrade")}
|
||||||
|
|
||||||
|
div.price-breakdown(ng-if="price.next.tax !== '0.00'")
|
||||||
|
hr.thin
|
||||||
|
span Total:
|
||||||
|
strong {{price.currency.symbol}}{{price.next.total}}
|
||||||
|
span ({{price.currency.symbol}}{{price.next.subtotal}} + {{price.currency.symbol}}{{price.next.tax}} tax)
|
||||||
|
span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")}
|
||||||
|
span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
|
||||||
|
hr.thin
|
||||||
|
|
||||||
span(sixpack-switch="payment-left-menu-bottom")
|
div.payment-submit
|
||||||
|
button.btn.btn-success.btn-block(
|
||||||
|
ng-click="submit()"
|
||||||
|
ng-disabled="processing || !isFormValid(simpleCCForm);"
|
||||||
|
sixpack-convert="payment-left-menu-bottom"
|
||||||
|
)
|
||||||
|
span(ng-show="processing")
|
||||||
|
i.fa.fa-spinner.fa-spin
|
||||||
|
|
|
||||||
|
| {{ paymentMethod.value === 'credit_card' ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_paypal_btn")}' }}
|
||||||
|
|
||||||
.col-md-3.col-md-pull-4(sixpack-default)
|
|
||||||
if showStudentPlan == 'true'
|
|
||||||
a.btn-primary.btn.plansPageStudentLink(
|
|
||||||
href,
|
|
||||||
ng-click="switchToStudent()"
|
|
||||||
) #{translate("half_price_student")}
|
|
||||||
|
|
||||||
.card.card-first
|
.col-md-3.col-md-pull-4
|
||||||
.paymentPageFeatures
|
if showStudentPlan == 'true'
|
||||||
h3 #{translate("unlimited_projects")}
|
a.btn-primary.btn.plansPageStudentLink(
|
||||||
p #{translate("create_unlimited_projects")}
|
href,
|
||||||
|
ng-click="switchToStudent()"
|
||||||
|
) #{translate("half_price_student")}
|
||||||
|
|
||||||
|
.card.card-first
|
||||||
|
.paymentPageFeatures
|
||||||
|
h3 #{translate("unlimited_projects")}
|
||||||
|
p #{translate("create_unlimited_projects")}
|
||||||
|
|
||||||
|
h3
|
||||||
|
if plan.features.collaborators == -1
|
||||||
|
- var collaboratorCount = 'Unlimited'
|
||||||
|
else
|
||||||
|
- var collaboratorCount = plan.features.collaborators
|
||||||
|
| #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
|
||||||
|
p #{translate("work_on_single_version")}. #{translate("view_collab_edits")} in real time.
|
||||||
|
|
||||||
h3
|
h3 #{translate("full_doc_history")}
|
||||||
if plan.features.collaborators == -1
|
p #{translate("see_what_has_been")}
|
||||||
- var collaboratorCount = 'Unlimited'
|
span.added #{translate("added")}
|
||||||
else
|
| #{translate("and")}
|
||||||
- var collaboratorCount = plan.features.collaborators
|
span.removed #{translate("removed")}.
|
||||||
| #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
|
| #{translate("restore_to_any_older_version")}.
|
||||||
p #{translate("work_on_single_version")}. #{translate("view_collab_edits")} in real time.
|
|
||||||
|
|
||||||
h3 #{translate("full_doc_history")}
|
|
||||||
p #{translate("see_what_has_been")}
|
|
||||||
span.added #{translate("added")}
|
|
||||||
| #{translate("and")}
|
|
||||||
span.removed #{translate("removed")}.
|
|
||||||
| #{translate("restore_to_any_older_version")}.
|
|
||||||
|
|
||||||
h3 #{translate("sync_to_dropbox")}
|
|
||||||
p
|
|
||||||
| #{translate("acces_work_from_anywhere")}.
|
|
||||||
| #{translate("work_offline_and_sync_with_dropbox")}.
|
|
||||||
|
|
||||||
hr
|
|
||||||
|
|
||||||
p.small.text-center We're confident that you'll love ShareLaTeX, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days.
|
|
||||||
hr
|
|
||||||
span
|
|
||||||
a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;")
|
|
||||||
img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0")
|
|
||||||
div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;")
|
|
||||||
a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;")
|
|
||||||
.col-md-3.col-md-pull-4(sixpack-when="bolder")
|
|
||||||
if showStudentPlan == 'true'
|
|
||||||
a.btn-primary.btn.plansPageStudentLink(
|
|
||||||
href,
|
|
||||||
ng-click="switchToStudent()"
|
|
||||||
) #{translate("half_price_student")}
|
|
||||||
|
|
||||||
.card.card-first
|
|
||||||
.paymentPageFeatures
|
|
||||||
.page-header
|
|
||||||
h2 #{translate("features")}
|
|
||||||
h3
|
|
||||||
i.fa.fa-check
|
|
||||||
| #{translate("unlimited_projects")}
|
|
||||||
|
|
||||||
h3
|
h3 #{translate("sync_to_dropbox")}
|
||||||
i.fa.fa-check
|
p
|
||||||
if plan.features.collaborators == -1
|
| #{translate("acces_work_from_anywhere")}.
|
||||||
- var collaboratorCount = 'Unlimited'
|
| #{translate("work_offline_and_sync_with_dropbox")}.
|
||||||
else
|
|
||||||
- var collaboratorCount = plan.features.collaborators
|
|
||||||
| #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
|
|
||||||
|
|
||||||
h3
|
hr
|
||||||
i.fa.fa-check
|
|
||||||
| #{translate("full_doc_history")}
|
|
||||||
|
|
||||||
h3
|
p.small.text-center We're confident that you'll love ShareLaTeX, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days.
|
||||||
i.fa.fa-check
|
hr
|
||||||
| #{translate("sync_to_dropbox")}
|
span
|
||||||
|
a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;")
|
||||||
h3
|
img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0")
|
||||||
i.fa.fa-check
|
div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;")
|
||||||
| #{translate("sync_to_github")}
|
a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;")
|
||||||
h3
|
|
||||||
i.fa.fa-check
|
|
||||||
| #{translate("Compile Larger Projects")}
|
|
||||||
hr
|
|
||||||
|
|
||||||
h2.text-center 30 Day Guarantee
|
|
||||||
hr
|
|
||||||
span
|
|
||||||
a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;")
|
|
||||||
img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0")
|
|
||||||
div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;")
|
|
||||||
a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;")
|
|
||||||
|
|
||||||
|
|
||||||
script(type="text/javascript").
|
script(type="text/javascript").
|
||||||
ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}")
|
ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}")
|
||||||
|
|
||||||
|
script(
|
||||||
|
type="text/ng-template"
|
||||||
|
id="cvv-tooltip-tpl.html"
|
||||||
|
)
|
||||||
|
p For #[strong Visa, MasterCard and Discover], the #[strong 3 digits] on the #[strong back] of your card.
|
||||||
|
p For #[strong American Express], the #[strong 4 digits] on the #[strong front] of your card.
|
||||||
|
|
||||||
mixin countries_options()
|
mixin countries_options()
|
||||||
option(value='', disabled, selected) #{translate("country")}
|
option(value='', disabled, selected) #{translate("country")}
|
||||||
option(value='-') --------------
|
option(value='-') --------------
|
||||||
|
|
|
@ -116,7 +116,7 @@ module.exports = settings =
|
||||||
|
|
||||||
# cdn:
|
# cdn:
|
||||||
# web:
|
# web:
|
||||||
# host:"http://cdn.sharelatex.dev:3000"
|
# host:"http://nowhere.sharelatex.dev"
|
||||||
# darkHost:"http://cdn.sharelatex.dev:3000"
|
# darkHost:"http://cdn.sharelatex.dev:3000"
|
||||||
|
|
||||||
# Where your instance of ShareLaTeX can be found publically. Used in emails
|
# Where your instance of ShareLaTeX can be found publically. Used in emails
|
||||||
|
|
539
services/web/public/coffee/directives/creditCards.coffee
Normal file
539
services/web/public/coffee/directives/creditCards.coffee
Normal file
|
@ -0,0 +1,539 @@
|
||||||
|
define [
|
||||||
|
"base"
|
||||||
|
], (App) ->
|
||||||
|
App.factory 'ccUtils', () ->
|
||||||
|
defaultFormat = /(\d{1,4})/g;
|
||||||
|
defaultInputFormat = /(?:^|\s)(\d{4})$/
|
||||||
|
|
||||||
|
cards = [
|
||||||
|
# Credit cards
|
||||||
|
{
|
||||||
|
type: 'visa'
|
||||||
|
patterns: [4]
|
||||||
|
format: defaultFormat
|
||||||
|
length: [13, 16]
|
||||||
|
cvcLength: [3]
|
||||||
|
luhn: true
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type: 'mastercard'
|
||||||
|
patterns: [
|
||||||
|
51, 52, 53, 54, 55,
|
||||||
|
22, 23, 24, 25, 26, 27
|
||||||
|
]
|
||||||
|
format: defaultFormat
|
||||||
|
length: [16]
|
||||||
|
cvcLength: [3]
|
||||||
|
luhn: true
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type: 'amex'
|
||||||
|
patterns: [34, 37]
|
||||||
|
format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/
|
||||||
|
length: [15]
|
||||||
|
cvcLength: [3..4]
|
||||||
|
luhn: true
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type: 'dinersclub'
|
||||||
|
patterns: [30, 36, 38, 39]
|
||||||
|
format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/
|
||||||
|
length: [14]
|
||||||
|
cvcLength: [3]
|
||||||
|
luhn: true
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type: 'discover'
|
||||||
|
patterns: [60, 64, 65, 622]
|
||||||
|
format: defaultFormat
|
||||||
|
length: [16]
|
||||||
|
cvcLength: [3]
|
||||||
|
luhn: true
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type: 'unionpay'
|
||||||
|
patterns: [62, 88]
|
||||||
|
format: defaultFormat
|
||||||
|
length: [16..19]
|
||||||
|
cvcLength: [3]
|
||||||
|
luhn: false
|
||||||
|
}
|
||||||
|
{
|
||||||
|
type: 'jcb'
|
||||||
|
patterns: [35]
|
||||||
|
format: defaultFormat
|
||||||
|
length: [16]
|
||||||
|
cvcLength: [3]
|
||||||
|
luhn: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
cardFromNumber = (num) ->
|
||||||
|
num = (num + '').replace(/\D/g, "")
|
||||||
|
for card in cards
|
||||||
|
for pattern in card.patterns
|
||||||
|
p = pattern + ""
|
||||||
|
return card if num.substr(0, p.length) == p
|
||||||
|
|
||||||
|
cardFromType = (type) ->
|
||||||
|
return card for card in cards when card.type is type
|
||||||
|
|
||||||
|
cardType = (num) ->
|
||||||
|
return null unless num
|
||||||
|
cardFromNumber(num)?.type or null
|
||||||
|
|
||||||
|
formatCardNumber = (num) ->
|
||||||
|
num = num.replace(/\D/g, '')
|
||||||
|
card = cardFromNumber(num)
|
||||||
|
return num unless card
|
||||||
|
|
||||||
|
upperLength = card.length[card.length.length - 1]
|
||||||
|
num = num[0...upperLength]
|
||||||
|
|
||||||
|
if card.format.global
|
||||||
|
num.match(card.format)?.join(' ')
|
||||||
|
else
|
||||||
|
groups = card.format.exec(num)
|
||||||
|
return unless groups?
|
||||||
|
groups.shift()
|
||||||
|
groups = $.grep(groups, (n) -> n) # Filter empty groups
|
||||||
|
groups.join(' ')
|
||||||
|
|
||||||
|
formatExpiry = (expiry) ->
|
||||||
|
parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/)
|
||||||
|
return '' unless parts
|
||||||
|
|
||||||
|
mon = parts[1] || ''
|
||||||
|
sep = parts[2] || ''
|
||||||
|
year = parts[3] || ''
|
||||||
|
|
||||||
|
if year.length > 0
|
||||||
|
sep = ' / '
|
||||||
|
|
||||||
|
else if sep is ' /'
|
||||||
|
mon = mon.substring(0, 1)
|
||||||
|
sep = ''
|
||||||
|
|
||||||
|
else if mon.length == 2 or sep.length > 0
|
||||||
|
sep = ' / '
|
||||||
|
|
||||||
|
else if mon.length == 1 and mon not in ['0', '1']
|
||||||
|
mon = "0#{mon}"
|
||||||
|
sep = ' / '
|
||||||
|
|
||||||
|
return mon + sep + year
|
||||||
|
|
||||||
|
parseExpiry = (value = "") ->
|
||||||
|
[month, year] = value.split(/[\s\/]+/, 2)
|
||||||
|
|
||||||
|
# Allow for year shortcut
|
||||||
|
if year?.length is 2 and /^\d+$/.test(year)
|
||||||
|
prefix = (new Date).getFullYear()
|
||||||
|
prefix = prefix.toString()[0..1]
|
||||||
|
year = prefix + year
|
||||||
|
|
||||||
|
month = parseInt(month, 10)
|
||||||
|
year = parseInt(year, 10)
|
||||||
|
|
||||||
|
return unless !isNaN(month) and !isNaN(year)
|
||||||
|
|
||||||
|
month: month, year: year
|
||||||
|
|
||||||
|
return {
|
||||||
|
fromNumber: cardFromNumber
|
||||||
|
fromType: cardFromType
|
||||||
|
cardType: cardType
|
||||||
|
formatExpiry: formatExpiry
|
||||||
|
formatCardNumber: formatCardNumber
|
||||||
|
defaultFormat: defaultFormat
|
||||||
|
defaultInputFormat: defaultInputFormat
|
||||||
|
parseExpiry: parseExpiry
|
||||||
|
}
|
||||||
|
|
||||||
|
App.factory 'ccFormat', (ccUtils, $filter) ->
|
||||||
|
hasTextSelected = ($target) ->
|
||||||
|
# If some text is selected
|
||||||
|
return true if $target.prop('selectionStart')? and
|
||||||
|
$target.prop('selectionStart') isnt $target.prop('selectionEnd')
|
||||||
|
|
||||||
|
# If some text is selected in IE
|
||||||
|
if document?.selection?.createRange?
|
||||||
|
return true if document.selection.createRange().text
|
||||||
|
|
||||||
|
false
|
||||||
|
|
||||||
|
safeVal = (value, $target) ->
|
||||||
|
try
|
||||||
|
cursor = $target.prop('selectionStart')
|
||||||
|
catch error
|
||||||
|
cursor = null
|
||||||
|
|
||||||
|
last = $target.val()
|
||||||
|
$target.val(value)
|
||||||
|
|
||||||
|
if cursor != null && $target.is(":focus")
|
||||||
|
cursor = value.length if cursor is last.length
|
||||||
|
|
||||||
|
# This hack looks for scenarios where we are changing an input's value such
|
||||||
|
# that "X| " is replaced with " |X" (where "|" is the cursor). In those
|
||||||
|
# scenarios, we want " X|".
|
||||||
|
#
|
||||||
|
# For example:
|
||||||
|
# 1. Input field has value "4444| "
|
||||||
|
# 2. User types "1"
|
||||||
|
# 3. Input field has value "44441| "
|
||||||
|
# 4. Reformatter changes it to "4444 |1"
|
||||||
|
# 5. By incrementing the cursor, we make it "4444 1|"
|
||||||
|
#
|
||||||
|
# This is awful, and ideally doesn't go here, but given the current design
|
||||||
|
# of the system there does not appear to be a better solution.
|
||||||
|
#
|
||||||
|
# Note that we can't just detect when the cursor-1 is " ", because that
|
||||||
|
# would incorrectly increment the cursor when backspacing, e.g. pressing
|
||||||
|
# backspace in this scenario: "4444 1|234 5".
|
||||||
|
if last != value
|
||||||
|
prevPair = last[cursor-1..cursor]
|
||||||
|
currPair = value[cursor-1..cursor]
|
||||||
|
digit = value[cursor]
|
||||||
|
cursor = cursor + 1 if /\d/.test(digit) and
|
||||||
|
prevPair == "#{digit} " and currPair == " #{digit}"
|
||||||
|
|
||||||
|
$target.prop('selectionStart', cursor)
|
||||||
|
$target.prop('selectionEnd', cursor)
|
||||||
|
|
||||||
|
# Replace Full-Width Chars
|
||||||
|
replaceFullWidthChars = (str = '') ->
|
||||||
|
fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'
|
||||||
|
halfWidth = '0123456789'
|
||||||
|
|
||||||
|
value = ''
|
||||||
|
chars = str.split('')
|
||||||
|
|
||||||
|
# Avoid using reserved word `char`
|
||||||
|
for chr in chars
|
||||||
|
idx = fullWidth.indexOf(chr)
|
||||||
|
chr = halfWidth[idx] if idx > -1
|
||||||
|
value += chr
|
||||||
|
|
||||||
|
value
|
||||||
|
|
||||||
|
# Format Numeric
|
||||||
|
reFormatNumeric = (e) ->
|
||||||
|
$target = $(e.currentTarget)
|
||||||
|
setTimeout ->
|
||||||
|
value = $target.val()
|
||||||
|
value = replaceFullWidthChars(value)
|
||||||
|
value = value.replace(/\D/g, '')
|
||||||
|
safeVal(value, $target)
|
||||||
|
|
||||||
|
# Format Card Number
|
||||||
|
reFormatCardNumber = (e) ->
|
||||||
|
$target = $(e.currentTarget)
|
||||||
|
setTimeout ->
|
||||||
|
value = $target.val()
|
||||||
|
value = replaceFullWidthChars(value)
|
||||||
|
value = ccUtils.formatCardNumber(value)
|
||||||
|
safeVal(value, $target)
|
||||||
|
|
||||||
|
formatCardNumber = (e) ->
|
||||||
|
# Only format if input is a number
|
||||||
|
digit = String.fromCharCode(e.which)
|
||||||
|
return unless /^\d+$/.test(digit)
|
||||||
|
|
||||||
|
$target = $(e.currentTarget)
|
||||||
|
value = $target.val()
|
||||||
|
card = ccUtils.fromNumber(value + digit)
|
||||||
|
length = (value.replace(/\D/g, '') + digit).length
|
||||||
|
|
||||||
|
upperLength = 16
|
||||||
|
upperLength = card.length[card.length.length - 1] if card
|
||||||
|
return if length >= upperLength
|
||||||
|
|
||||||
|
# Return if focus isn't at the end of the text
|
||||||
|
return if $target.prop('selectionStart')? and
|
||||||
|
$target.prop('selectionStart') isnt value.length
|
||||||
|
|
||||||
|
if card && card.type is 'amex'
|
||||||
|
# AMEX cards are formatted differently
|
||||||
|
re = /^(\d{4}|\d{4}\s\d{6})$/
|
||||||
|
else
|
||||||
|
re = /(?:^|\s)(\d{4})$/
|
||||||
|
|
||||||
|
# If '4242' + 4
|
||||||
|
if re.test(value)
|
||||||
|
e.preventDefault()
|
||||||
|
setTimeout -> $target.val(value + ' ' + digit)
|
||||||
|
|
||||||
|
# If '424' + 2
|
||||||
|
else if re.test(value + digit)
|
||||||
|
e.preventDefault()
|
||||||
|
setTimeout -> $target.val(value + digit + ' ')
|
||||||
|
|
||||||
|
formatBackCardNumber = (e) ->
|
||||||
|
$target = $(e.currentTarget)
|
||||||
|
value = $target.val()
|
||||||
|
|
||||||
|
# Return unless backspacing
|
||||||
|
return unless e.which is 8
|
||||||
|
|
||||||
|
# Return if focus isn't at the end of the text
|
||||||
|
return if $target.prop('selectionStart')? and
|
||||||
|
$target.prop('selectionStart') isnt value.length
|
||||||
|
|
||||||
|
# Remove the digit + trailing space
|
||||||
|
if /\d\s$/.test(value)
|
||||||
|
e.preventDefault()
|
||||||
|
setTimeout -> $target.val(value.replace(/\d\s$/, ''))
|
||||||
|
# Remove digit if ends in space + digit
|
||||||
|
else if /\s\d?$/.test(value)
|
||||||
|
e.preventDefault()
|
||||||
|
setTimeout -> $target.val(value.replace(/\d$/, ''))
|
||||||
|
|
||||||
|
getFormattedCardNumber = (num) ->
|
||||||
|
num = num.replace(/\D/g, '')
|
||||||
|
card = ccUtils.fromNumber(num)
|
||||||
|
return num unless card
|
||||||
|
|
||||||
|
upperLength = card.length[card.length.length - 1]
|
||||||
|
num = num[0...upperLength]
|
||||||
|
|
||||||
|
if card.format.global
|
||||||
|
num.match(card.format)?.join(' ')
|
||||||
|
else
|
||||||
|
groups = card.format.exec(num)
|
||||||
|
return unless groups?
|
||||||
|
groups.shift()
|
||||||
|
groups = $.grep(groups, (n) -> n) # Filter empty groups
|
||||||
|
groups.join(' ')
|
||||||
|
|
||||||
|
parseCardNumber = (value) ->
|
||||||
|
if value? then value.replace(/\s/g, '') else value
|
||||||
|
|
||||||
|
# Format Expiry
|
||||||
|
reFormatExpiry = (e) ->
|
||||||
|
$target = $(e.currentTarget)
|
||||||
|
setTimeout ->
|
||||||
|
value = $target.val()
|
||||||
|
value = replaceFullWidthChars(value)
|
||||||
|
value = ccUtils.formatExpiry(value)
|
||||||
|
safeVal(value, $target)
|
||||||
|
|
||||||
|
|
||||||
|
formatExpiry = (e) ->
|
||||||
|
# Only format if input is a number
|
||||||
|
digit = String.fromCharCode(e.which)
|
||||||
|
return unless /^\d+$/.test(digit)
|
||||||
|
|
||||||
|
$target = $(e.currentTarget)
|
||||||
|
val = $target.val() + digit
|
||||||
|
|
||||||
|
if /^\d$/.test(val) and val not in ['0', '1']
|
||||||
|
e.preventDefault()
|
||||||
|
setTimeout -> $target.val("0#{val} / ")
|
||||||
|
|
||||||
|
else if /^\d\d$/.test(val)
|
||||||
|
e.preventDefault()
|
||||||
|
setTimeout ->
|
||||||
|
# Split for months where we have the second digit > 2 (past 12) and turn
|
||||||
|
# that into (m1)(m2) => 0(m1) / (m2)
|
||||||
|
m1 = parseInt(val[0], 10)
|
||||||
|
m2 = parseInt(val[1], 10)
|
||||||
|
if m2 > 2 and m1 != 0
|
||||||
|
$target.val("0#{m1} / #{m2}")
|
||||||
|
else
|
||||||
|
$target.val("#{val} / ")
|
||||||
|
|
||||||
|
formatForwardExpiry = (e) ->
|
||||||
|
digit = String.fromCharCode(e.which)
|
||||||
|
return unless /^\d+$/.test(digit)
|
||||||
|
|
||||||
|
$target = $(e.currentTarget)
|
||||||
|
val = $target.val()
|
||||||
|
|
||||||
|
if /^\d\d$/.test(val)
|
||||||
|
$target.val("#{val} / ")
|
||||||
|
|
||||||
|
formatForwardSlash = (e) ->
|
||||||
|
which = String.fromCharCode(e.which)
|
||||||
|
return unless which is '/' or which is ' '
|
||||||
|
|
||||||
|
$target = $(e.currentTarget)
|
||||||
|
val = $target.val()
|
||||||
|
|
||||||
|
if /^\d$/.test(val) and val isnt '0'
|
||||||
|
$target.val("0#{val} / ")
|
||||||
|
|
||||||
|
formatBackExpiry = (e) ->
|
||||||
|
$target = $(e.currentTarget)
|
||||||
|
value = $target.val()
|
||||||
|
|
||||||
|
# Return unless backspacing
|
||||||
|
return unless e.which is 8
|
||||||
|
|
||||||
|
# Return if focus isn't at the end of the text
|
||||||
|
return if $target.prop('selectionStart')? and
|
||||||
|
$target.prop('selectionStart') isnt value.length
|
||||||
|
|
||||||
|
# Remove the trailing space + last digit
|
||||||
|
if /\d\s\/\s$/.test(value)
|
||||||
|
e.preventDefault()
|
||||||
|
setTimeout -> $target.val(value.replace(/\d\s\/\s$/, ''))
|
||||||
|
|
||||||
|
parseExpiry = (value) ->
|
||||||
|
if value?
|
||||||
|
dateAsObj = ccUtils.parseExpiry(value)
|
||||||
|
|
||||||
|
return unless dateAsObj?
|
||||||
|
|
||||||
|
expiry = new Date dateAsObj.year, dateAsObj.month - 1
|
||||||
|
|
||||||
|
return $filter('date')(expiry, 'MM/yyyy')
|
||||||
|
|
||||||
|
# Format CVC
|
||||||
|
reFormatCVC = (e) ->
|
||||||
|
$target = $(e.currentTarget)
|
||||||
|
setTimeout ->
|
||||||
|
value = $target.val()
|
||||||
|
value = replaceFullWidthChars(value)
|
||||||
|
value = value.replace(/\D/g, '')[0...4]
|
||||||
|
safeVal(value, $target)
|
||||||
|
|
||||||
|
# Restrictions
|
||||||
|
restrictNumeric = (e) ->
|
||||||
|
# Key event is for a browser shortcut
|
||||||
|
return true if e.metaKey or e.ctrlKey
|
||||||
|
|
||||||
|
# If keycode is a space
|
||||||
|
return false if e.which is 32
|
||||||
|
|
||||||
|
# If keycode is a special char (WebKit)
|
||||||
|
return true if e.which is 0
|
||||||
|
|
||||||
|
# If char is a special char (Firefox)
|
||||||
|
return true if e.which < 33
|
||||||
|
|
||||||
|
input = String.fromCharCode(e.which)
|
||||||
|
|
||||||
|
# Char is a number or a space
|
||||||
|
!!/[\d\s]/.test(input)
|
||||||
|
|
||||||
|
restrictCardNumber = (e) ->
|
||||||
|
$target = $(e.currentTarget)
|
||||||
|
digit = String.fromCharCode(e.which)
|
||||||
|
return unless /^\d+$/.test(digit)
|
||||||
|
|
||||||
|
return if hasTextSelected($target)
|
||||||
|
|
||||||
|
# Restrict number of digits
|
||||||
|
value = ($target.val() + digit).replace(/\D/g, '')
|
||||||
|
card = ccUtils.fromNumber(value)
|
||||||
|
|
||||||
|
if card
|
||||||
|
value.length <= card.length[card.length.length - 1]
|
||||||
|
else
|
||||||
|
# All other cards are 16 digits long
|
||||||
|
value.length <= 16
|
||||||
|
|
||||||
|
restrictExpiry = (e) ->
|
||||||
|
$target = $(e.currentTarget)
|
||||||
|
digit = String.fromCharCode(e.which)
|
||||||
|
return unless /^\d+$/.test(digit)
|
||||||
|
|
||||||
|
return if hasTextSelected($target)
|
||||||
|
|
||||||
|
value = $target.val() + digit
|
||||||
|
value = value.replace(/\D/g, '')
|
||||||
|
|
||||||
|
return false if value.length > 6
|
||||||
|
|
||||||
|
restrictCVC = (e) ->
|
||||||
|
$target = $(e.currentTarget)
|
||||||
|
digit = String.fromCharCode(e.which)
|
||||||
|
return unless /^\d+$/.test(digit)
|
||||||
|
|
||||||
|
return if hasTextSelected($target)
|
||||||
|
|
||||||
|
val = $target.val() + digit
|
||||||
|
val.length <= 4
|
||||||
|
|
||||||
|
setCardType = (e) ->
|
||||||
|
$target = $(e.currentTarget)
|
||||||
|
val = $target.val()
|
||||||
|
cardType = ccUtils.cardType(val) or 'unknown'
|
||||||
|
|
||||||
|
unless $target.hasClass(cardType)
|
||||||
|
allTypes = (card.type for card in cards)
|
||||||
|
|
||||||
|
$target.removeClass('unknown')
|
||||||
|
$target.removeClass(allTypes.join(' '))
|
||||||
|
|
||||||
|
$target.addClass(cardType)
|
||||||
|
$target.toggleClass('identified', cardType isnt 'unknown')
|
||||||
|
$target.trigger('payment.cardType', cardType)
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasTextSelected
|
||||||
|
replaceFullWidthChars
|
||||||
|
reFormatNumeric
|
||||||
|
reFormatCardNumber
|
||||||
|
formatCardNumber
|
||||||
|
formatBackCardNumber
|
||||||
|
getFormattedCardNumber
|
||||||
|
parseCardNumber
|
||||||
|
reFormatExpiry
|
||||||
|
formatExpiry
|
||||||
|
formatForwardExpiry
|
||||||
|
formatForwardSlash
|
||||||
|
formatBackExpiry
|
||||||
|
parseExpiry
|
||||||
|
reFormatCVC
|
||||||
|
restrictNumeric
|
||||||
|
restrictCardNumber
|
||||||
|
restrictExpiry
|
||||||
|
restrictCVC
|
||||||
|
setCardType
|
||||||
|
}
|
||||||
|
|
||||||
|
App.directive "ccFormatExpiry", (ccFormat) ->
|
||||||
|
restrict: "A"
|
||||||
|
require: "ngModel"
|
||||||
|
link: (scope, el, attrs, ngModel) ->
|
||||||
|
el.on "keypress", ccFormat.restrictNumeric
|
||||||
|
el.on "keypress", ccFormat.restrictExpiry
|
||||||
|
el.on "keypress", ccFormat.formatExpiry
|
||||||
|
el.on "keypress", ccFormat.formatForwardSlash
|
||||||
|
el.on "keypress", ccFormat.formatForwardExpiry
|
||||||
|
el.on "keydown", ccFormat.formatBackExpiry
|
||||||
|
el.on "change", ccFormat.reFormatExpiry
|
||||||
|
el.on "input", ccFormat.reFormatExpiry
|
||||||
|
el.on "paste", ccFormat.reFormatExpiry
|
||||||
|
|
||||||
|
ngModel.$parsers.push ccFormat.parseExpiry
|
||||||
|
ngModel.$formatters.push ccFormat.parseExpiry
|
||||||
|
|
||||||
|
App.directive "ccFormatCardNumber", (ccFormat) ->
|
||||||
|
restrict: "A"
|
||||||
|
require: "ngModel"
|
||||||
|
link: (scope, el, attrs, ngModel) ->
|
||||||
|
el.on "keypress", ccFormat.restrictNumeric
|
||||||
|
el.on "keypress", ccFormat.restrictCardNumber
|
||||||
|
el.on "keypress", ccFormat.formatCardNumber
|
||||||
|
el.on "keydown", ccFormat.formatBackCardNumber
|
||||||
|
el.on "paste", ccFormat.reFormatCardNumber
|
||||||
|
|
||||||
|
ngModel.$parsers.push ccFormat.parseCardNumber
|
||||||
|
ngModel.$formatters.push ccFormat.getFormattedCardNumber
|
||||||
|
|
||||||
|
App.directive "ccFormatSecCode", (ccFormat) ->
|
||||||
|
restrict: "A"
|
||||||
|
require: "ngModel"
|
||||||
|
link: (scope, el, attrs, ngModel) ->
|
||||||
|
el.on "keypress", ccFormat.restrictNumeric
|
||||||
|
el.on "keypress", ccFormat.restrictCVC
|
||||||
|
el.on "paste", ccFormat.reFormatCVC
|
||||||
|
el.on "change", ccFormat.reFormatCVC
|
||||||
|
el.on "input", ccFormat.reFormatCVC
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ define [
|
||||||
"directives/onEnter"
|
"directives/onEnter"
|
||||||
"directives/selectAll"
|
"directives/selectAll"
|
||||||
"directives/maxHeight"
|
"directives/maxHeight"
|
||||||
|
"directives/creditCards"
|
||||||
"services/queued-http"
|
"services/queued-http"
|
||||||
"filters/formatDate"
|
"filters/formatDate"
|
||||||
"__MAIN_CLIENTSIDE_INCLUDES__"
|
"__MAIN_CLIENTSIDE_INCLUDES__"
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
define [
|
define [
|
||||||
"base"
|
"base",
|
||||||
|
"directives/creditCards"
|
||||||
], (App)->
|
], (App)->
|
||||||
|
|
||||||
App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking)->
|
App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking, ccUtils)->
|
||||||
throw new Error("Recurly API Library Missing.") if typeof recurly is "undefined"
|
throw new Error("Recurly API Library Missing.") if typeof recurly is "undefined"
|
||||||
|
|
||||||
$scope.currencyCode = MultiCurrencyPricing.currencyCode
|
$scope.currencyCode = MultiCurrencyPricing.currencyCode
|
||||||
|
@ -11,8 +12,10 @@ define [
|
||||||
$scope.switchToStudent = ()->
|
$scope.switchToStudent = ()->
|
||||||
window.location = "/user/subscription/new?planCode=student_free_trial_7_days¤cy=#{$scope.currencyCode}&cc=#{$scope.data.coupon}"
|
window.location = "/user/subscription/new?planCode=student_free_trial_7_days¤cy=#{$scope.currencyCode}&cc=#{$scope.data.coupon}"
|
||||||
|
|
||||||
|
event_tracking.sendMB "subscription-form", { plan : window.plan_code }
|
||||||
|
|
||||||
$scope.paymentMethod = "credit_card"
|
$scope.paymentMethod =
|
||||||
|
value: "credit_card"
|
||||||
|
|
||||||
$scope.data =
|
$scope.data =
|
||||||
number: ""
|
number: ""
|
||||||
|
@ -28,12 +31,12 @@ define [
|
||||||
city:""
|
city:""
|
||||||
country:window.countryCode
|
country:window.countryCode
|
||||||
coupon: window.couponCode
|
coupon: window.couponCode
|
||||||
|
mmYY: ""
|
||||||
|
|
||||||
|
|
||||||
$scope.validation =
|
$scope.validation =
|
||||||
correctCardNumber : true
|
correctCardNumber : true
|
||||||
correctExpiry: true
|
correctExpiry: true
|
||||||
correctCvv:true
|
correctCvv: true
|
||||||
|
|
||||||
$scope.processing = false
|
$scope.processing = false
|
||||||
|
|
||||||
|
@ -51,12 +54,11 @@ define [
|
||||||
.done()
|
.done()
|
||||||
|
|
||||||
pricing.on "change", =>
|
pricing.on "change", =>
|
||||||
event_tracking.sendMB "subscription-form", { plan : pricing.items.plan.code }
|
|
||||||
|
|
||||||
$scope.planName = pricing.items.plan.name
|
$scope.planName = pricing.items.plan.name
|
||||||
$scope.price = pricing.price
|
$scope.price = pricing.price
|
||||||
$scope.trialLength = pricing.items.plan.trial?.length
|
$scope.trialLength = pricing.items.plan.trial?.length
|
||||||
$scope.monthlyBilling = pricing.items.plan.period.length == 1
|
$scope.monthlyBilling = pricing.items.plan.period.length == 1
|
||||||
|
|
||||||
if pricing.items?.coupon?.discount?.type == "percent"
|
if pricing.items?.coupon?.discount?.type == "percent"
|
||||||
basePrice = parseInt(pricing.price.base.plan.unit)
|
basePrice = parseInt(pricing.price.base.plan.unit)
|
||||||
$scope.normalPrice = basePrice
|
$scope.normalPrice = basePrice
|
||||||
|
@ -74,35 +76,58 @@ define [
|
||||||
$scope.applyVatNumber = ->
|
$scope.applyVatNumber = ->
|
||||||
pricing.tax({tax_code: 'digital', vat_number: $scope.data.vat_number}).done()
|
pricing.tax({tax_code: 'digital', vat_number: $scope.data.vat_number}).done()
|
||||||
|
|
||||||
|
|
||||||
$scope.changeCurrency = (newCurrency)->
|
$scope.changeCurrency = (newCurrency)->
|
||||||
$scope.currencyCode = newCurrency
|
$scope.currencyCode = newCurrency
|
||||||
pricing.currency(newCurrency).done()
|
pricing.currency(newCurrency).done()
|
||||||
|
|
||||||
|
$scope.updateExpiry = () ->
|
||||||
|
parsedDateObj = ccUtils.parseExpiry $scope.data.mmYY
|
||||||
|
if parsedDateObj?
|
||||||
|
$scope.data.month = parsedDateObj.month
|
||||||
|
$scope.data.year = parsedDateObj.year
|
||||||
|
|
||||||
$scope.validateCardNumber = validateCardNumber = ->
|
$scope.validateCardNumber = validateCardNumber = ->
|
||||||
|
$scope.validation.errorFields = {}
|
||||||
if $scope.data.number?.length != 0
|
if $scope.data.number?.length != 0
|
||||||
$scope.validation.correctCardNumber = recurly.validate.cardNumber($scope.data.number)
|
$scope.validation.correctCardNumber = recurly.validate.cardNumber($scope.data.number)
|
||||||
|
|
||||||
$scope.validateExpiry = validateExpiry = ->
|
$scope.validateExpiry = validateExpiry = ->
|
||||||
|
$scope.validation.errorFields = {}
|
||||||
if $scope.data.month?.length != 0 and $scope.data.year?.length != 0
|
if $scope.data.month?.length != 0 and $scope.data.year?.length != 0
|
||||||
$scope.validation.correctExpiry = recurly.validate.expiry($scope.data.month, $scope.data.year)
|
$scope.validation.correctExpiry = recurly.validate.expiry($scope.data.month, $scope.data.year)
|
||||||
|
|
||||||
$scope.validateCvv = validateCvv = ->
|
$scope.validateCvv = validateCvv = ->
|
||||||
|
$scope.validation.errorFields = {}
|
||||||
if $scope.data.cvv?.length != 0
|
if $scope.data.cvv?.length != 0
|
||||||
$scope.validation.correctCvv = recurly.validate.cvv($scope.data.cvv)
|
$scope.validation.correctCvv = recurly.validate.cvv($scope.data.cvv)
|
||||||
|
|
||||||
|
$scope.inputHasError = inputHasError = (formItem) ->
|
||||||
|
if !formItem?
|
||||||
|
return false
|
||||||
|
|
||||||
|
return (formItem.$touched && formItem.$invalid)
|
||||||
|
|
||||||
|
$scope.isFormValid = isFormValid = (form) ->
|
||||||
|
if $scope.paymentMethod.value == 'paypal'
|
||||||
|
return $scope.data.country != ""
|
||||||
|
else
|
||||||
|
return (form.$valid and
|
||||||
|
$scope.validation.correctCardNumber and
|
||||||
|
$scope.validation.correctExpiry and
|
||||||
|
$scope.validation.correctCvv)
|
||||||
|
|
||||||
$scope.updateCountry = ->
|
$scope.updateCountry = ->
|
||||||
pricing.address({country:$scope.data.country}).done()
|
pricing.address({country:$scope.data.country}).done()
|
||||||
|
|
||||||
$scope.changePaymentMethod = (paymentMethod)->
|
$scope.setPaymentMethod = setPaymentMethod = (method) ->
|
||||||
if paymentMethod == "paypal"
|
$scope.paymentMethod.value = method;
|
||||||
$scope.usePaypal = true
|
$scope.validation.errorFields = {}
|
||||||
else
|
$scope.genericError = ""
|
||||||
$scope.usePaypal = false
|
|
||||||
|
|
||||||
completeSubscription = (err, recurly_token_id) ->
|
completeSubscription = (err, recurly_token_id) ->
|
||||||
$scope.validation.errorFields = {}
|
$scope.validation.errorFields = {}
|
||||||
if err?
|
if err?
|
||||||
|
event_tracking.sendMB "subscription-error", err
|
||||||
# We may or may not be in a digest loop here depending on
|
# We may or may not be in a digest loop here depending on
|
||||||
# whether recurly could do validation locally, so do it async
|
# whether recurly could do validation locally, so do it async
|
||||||
$scope.$evalAsync () ->
|
$scope.$evalAsync () ->
|
||||||
|
@ -117,7 +142,7 @@ define [
|
||||||
currencyCode:pricing.items.currency
|
currencyCode:pricing.items.currency
|
||||||
plan_code:pricing.items.plan.code
|
plan_code:pricing.items.plan.code
|
||||||
coupon_code:pricing.items?.coupon?.code || ""
|
coupon_code:pricing.items?.coupon?.code || ""
|
||||||
isPaypal: $scope.paymentMethod == 'paypal'
|
isPaypal: $scope.paymentMethod.value == 'paypal'
|
||||||
address:
|
address:
|
||||||
address1: $scope.data.address1
|
address1: $scope.data.address1
|
||||||
address2: $scope.data.address2
|
address2: $scope.data.address2
|
||||||
|
@ -132,6 +157,8 @@ define [
|
||||||
isPaypal : postData.subscriptionDetails.isPaypal
|
isPaypal : postData.subscriptionDetails.isPaypal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sixpack.convert "subscription-form"
|
||||||
|
|
||||||
$http.post("/user/subscription/create", postData)
|
$http.post("/user/subscription/create", postData)
|
||||||
.success (data, status, headers)->
|
.success (data, status, headers)->
|
||||||
sixpack.convert "in-editor-free-trial-plan", pricing.items.plan.code, (err)->
|
sixpack.convert "in-editor-free-trial-plan", pricing.items.plan.code, (err)->
|
||||||
|
@ -143,7 +170,7 @@ define [
|
||||||
|
|
||||||
$scope.submit = ->
|
$scope.submit = ->
|
||||||
$scope.processing = true
|
$scope.processing = true
|
||||||
if $scope.paymentMethod == 'paypal'
|
if $scope.paymentMethod.value == 'paypal'
|
||||||
opts = { description: $scope.planName }
|
opts = { description: $scope.planName }
|
||||||
recurly.paypal opts, completeSubscription
|
recurly.paypal opts, completeSubscription
|
||||||
else
|
else
|
||||||
|
|
|
@ -89,8 +89,27 @@
|
||||||
.small {
|
.small {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
margin-top: (@line-height-computed / 2);
|
||||||
|
margin-bottom: (@line-height-computed / 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-check,
|
||||||
|
.features-copy {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12%;
|
||||||
|
line-height: 1.4;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-copy {
|
||||||
|
width: 88%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.plansPageStudentLink {
|
.plansPageStudentLink {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
|
74
services/web/public/stylesheets/app/subscription.less
Normal file
74
services/web/public/stylesheets/app/subscription.less
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
.form-helper {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1.3em;
|
||||||
|
height: 1.3em;
|
||||||
|
line-height: 1.3;
|
||||||
|
vertical-align: initial;
|
||||||
|
background-color: @gray;
|
||||||
|
color: #FFF;
|
||||||
|
font-weight: bolder;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: #FFF;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-breakdown {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-feedback-message {
|
||||||
|
display: none;
|
||||||
|
font-size: 0.8em;
|
||||||
|
|
||||||
|
.has-error & {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-submit {
|
||||||
|
padding-top: (@line-height-computed / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-method-toggle {
|
||||||
|
margin-bottom: (@line-height-computed / 2);
|
||||||
|
|
||||||
|
&-switch {
|
||||||
|
display: inline-block;
|
||||||
|
width: 50%;
|
||||||
|
text-align: center;
|
||||||
|
border: solid 1px @gray-lighter;
|
||||||
|
border-radius: @border-radius-large 0 0 @border-radius-large;
|
||||||
|
padding: (@line-height-computed / 2);
|
||||||
|
color: @gray;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: @gray;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @gray-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + & {
|
||||||
|
border-left-width: 0;
|
||||||
|
border-radius: 0 @border-radius-large @border-radius-large 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-selected {
|
||||||
|
color: @link-color;
|
||||||
|
box-shadow: inset 0 -2px 0 0;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: @link-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -75,6 +75,7 @@
|
||||||
@import "app/wiki.less";
|
@import "app/wiki.less";
|
||||||
@import "app/translations.less";
|
@import "app/translations.less";
|
||||||
@import "app/contact-us.less";
|
@import "app/contact-us.less";
|
||||||
|
@import "app/subscription.less";
|
||||||
@import "app/sprites.less";
|
@import "app/sprites.less";
|
||||||
@import "app/invite.less";
|
@import "app/invite.less";
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue