- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 270
- 271
- 272
- 273
- 274
- 275
- 276
- 277
- 278
- 279
- 280
- 281
- 282
- 283
- 284
- 285
- 286
- 287
- 288
- 289
- 290
- 291
- 292
- 293
- 294
- 295
- 296
- 297
- 298
- 299
- 300
- 301
- 302
- 303
- 304
- 305
- 306
- 307
- 308
- 309
- 310
- 311
- 312
- 313
- 314
- 315
- 316
- 317
- 318
- 319
- 320
- 321
- 322
- 323
- 324
- 325
- 326
- 327
- 328
- 329
- 330
- 331
- 332
- 333
- 334
- 335
- 336
- 337
- 338
- 339
- 340
- 341
- 342
- 343
- 344
- 345
- 346
- 347
- 348
- 349
- 350
- 351
- 352
- 353
- 354
- 355
- 356
- 357
- 358
- 359
- 360
- 361
- 362
- 363
- 364
- 365
- 366
- 367
- 368
- 369
- 370
- 371
- 372
- 373
- 374
- 375
- 376
- 377
- 378
- 379
- 380
- 381
- 382
- 383
- 384
- 385
- 386
- 387
- 388
- 389
- 390
- 391
- 392
- 393
- 394
- 395
- 396
- 397
- 398
- 399
- 400
- 401
- 402
- 403
- 404
- 405
- 406
- 407
- 408
- 409
- 410
- 411
- 412
- 413
- 414
- 415
- 416
- 417
- 418
- 419
- 420
- 421
- 422
- 423
- 424
- 425
- 426
- 427
- 428
- 429
- 430
- 431
- 432
- 433
- 434
- 435
- 436
- 437
- 438
- 439
- 440
- 441
- 442
- 443
- 444
- 445
- 446
- 447
- 448
- 449
- 450
- 451
- 452
- 453
- 454
- 455
- 456
- 457
- 458
- 459
- 460
- 461
- 462
- 463
- 464
- 465
- 466
- 467
- 468
- 469
- 470
- 471
- 472
- 473
- 474
- 475
- 476
- 477
- 478
- 479
- 480
- 481
- 482
- 483
- 484
- 485
- 486
- 487
- 488
- 489
- 490
- 491
- 492
- 493
- 494
- 495
- 496
- 497
- 498
- 499
- 500
- 501
- 502
- 503
- 504
- 505
- 506
- 507
- 508
- 509
- 510
- 511
- 512
- 513
- 514
- 515
- 516
- 517
- 518
- 519
- 520
- 521
- 522
- 523
- 524
- 525
- 526
- 527
- 528
- 529
- 530
- 531
- 532
- 533
- 534
- 535
- 536
- 537
- 538
- 539
- 540
- 541
- 542
- 543
- 544
- 545
- 546
- 547
- 548
- 549
- 550
- 551
- 552
- 553
- 554
- 555
- 556
- 557
- 558
- 559
- 560
- 561
- 562
- 563
- 564
- 565
- 566
- 567
- 568
- 569
- 570
- 571
- 572
- 573
- 574
- 575
- 576
- 577
- 578
- 579
- 580
- 581
- 582
- 583
- 584
- 585
- 586
- 587
- 588
- 589
- 590
- 591
- 592
- 593
- 594
- 595
- 596
- 597
- 598
- 599
- 600
- 601
- 602
- 603
- 604
- 605
- 606
- 607
- 608
- 609
- 610
- 611
- 612
- 613
- 614
- 615
- 616
- 617
- 618
- 619
- 620
- 621
- 622
- 623
<template>
<div class="product-wrap">
<section class="product" :data-level='this.level'>
<div class="ribbontext" v-if='productRibbonText'>
<span>{{ productRibbonText }}</span>
</div>
<div class="header">
<div>{{ productName }}</div>
</div>
<div class="body">
<div class="offertext" v-if='productOfferText'>{{ productOfferText }}</div>
<div class="pricing">
<div class="monthly">
<div class="dollar"><span>$</span><span>{{ dollars }}</span></div>
<div class="cents" v-if="cents">
<sup>{{ cents }}</sup>
<span v-if="!this.productShowFullPrice">/{{monthText}}</span>
</div>
</div> <!--/ end .monthly -->
</div> <!--/ end .pricing -->
<!-- msrp text shown underneath the price -->
<div class="original-price" v-if='!productShowFullPrice'>
<span class="msrp" v-if='msrp != price'>${{ this.msrp }}</span>
<span class="price">${{ this.price }}</span>
<span>{{ dueText }}</span>
</div>
<!-- With fullprice=1, we display `Regular $<strikethrough msrp>` -->
<div class="original-price" v-else>
<span>Regular</span>
<span class="msrp">${{ this.msrp }}</span>
</div>
</div> <!-- /end .body -->
<div class="footer">
<a :href="productCartLink" class="add-to-cart" @click='onAddToCartClick'></a>
</div>
</section>
<div class="offertext-mobile" v-if='offerText'>{{ offerText }}</div>
<div class="cart-iframe-wrap">
hi
</div>
</div>
</template>
<script>
/**
* This is a single product box. It shows one product.
* Media queries regarding layout should be done on the page
* that is including this component.
*
* Query params override settings from Airtable.
*
* Current accepted URL params: *
*
* ?showfull=1
* Will show full price in every product instead of monthly
*
* ?pn={_24_:_buy 24 get 24 months free!_}
* Will update the product name for 24m.
*
* ?ribbontext={_24_:_best offer!_}
* Ribbon text is the text on top of the product box
*
* ?offertext={_24_:_offer ends soon_}
* The text hat is inside the (usually green) box
* between the product name and price.
*
* ?xpay={_24_:_3_}
* Will format 24m cart link to apply 3-pay in cart.
* Note this can also be 5 pay. ie, xpay={24:5}.
* It will also divide price by the X amount to show
* in the html.
*
*
*/
export default {
/* Props passed from parent. ie,
<ProductBox v-for="(product, key) in products"
:msrp='product.msrp'
:price='product.price'
:level='product.lvl'
:cartLink='product.cart'
:showFullPrice='parseInt($route.query.showfull) || product.fullprice'
:bonusMonth="bonusMonths && parseInt(bonusMonths[product.lvl]) || null"
:xpay="xpay && parseInt(xpay[product.lvl]) || null"
:ribbonText="ribbonText && ribbonText[product.lvl] || null"
:offerText="offerText && offerText[product.lvl] || null"
:name="productNames && productNames[product.lvl] || null"
:products=productList
iframeCart=true
:key=key
/>
*/
props: {
// MSRP price of this product
msrp: {
type: [String, Number],
required: true
},
// Sale price of this product
price: {
type: [String, Number],
required: true,
},
// The level of this product (6, 12, 24, etc)
level: {
type: [String, Number],
required: true,
},
// The Magento cart url
cartLink: {
type: String,
required: true
},
// The name of this product. If blank, this component will
// use the level as its name. Can be overwritten by url
name: {
type: String,
required: false
},
// If true, price will be showed as a whole, not monthly.
// Can be overwritten by url
showFullPrice: {
type: Number,
required: false,
default: false
},
// Should be a number. Will update Magento cart link with pp/x/?pc...
// Can be overwritten by url
xpay: {
type: Number,
required: false,
default: null
},
// Bonus month this product becomes.
// Can be overwritten by url
bonusMonth: {
type: Boolean,
required: false,
default: false,
},
// Text to show in the ribbon area
// Can be overwritten by url
ribbonText: {
type: String,
required: false,
default: null
},
// Text to show in the offer area
// Can be overwritten by url
offerText: {
type: String,
required: false,
default: false
},
// An array of all products. This is currently not in use.
products: {
type: Array,
required: false,
default: null
},
// Shows cart inside iframe instead of redirecting to cart
iframeCart: {
type: Boolean,
required: false,
default: true,
}
},
data()
{
return {
dollars: null,
cents: null,
// What we'll be dividing prices by to calculate monthly. use level by default
divideBy: this.level,
// Text displayed next to price - TODO store this in Airtable somewhere
dueText: this.$store.state.locale == 'en' ? "due today" : 'paga hoy',
// Varies by locale - TODO store this in Airtable somewhere
monthText: this.$store.state.locale == 'en' ? 'Month' : 'Mes',
// These will be used in the <template>. We use these instead of the
// props passed because Vue doesn't want you to mutate props. So
// when a url param requires a prop to be changed, we must
// use one of these data values instead of mutating the prop
// directly. Justt remember, don't try to mutate props.
productShowFullPrice: this.showFullPrice,
productOfferText: this.offerText,
productRibbonText: this.ribbonText,
productCartLink: this.cartLink,
}
},
created ()
{
// By default, show monthly pricing: price / level (ie, 150/12 for 12mo)
// But if there's even just 1 lifetime product, all pricing becomes full price
if (/lifetime/ig.test(this.productName)) {
this.productShowFullPrice = 1
this.divideBy = 1
}
// Determine our monthly price: price/level
const monthly = (Math.ceil(parseFloat(this.price, 10)
/ parseInt(this.divideBy, 10) * 100)
/ 100)
.toFixed(2).split('.')
// Set our dollars and cents for html view
this.dollars = monthly[0]
this.cents = monthly[1] == undefined ? '00' : monthly[1]
this.cents = this.cents == '00' ? null : this.cents
// If showFullPrice is true, don't show monthly pricing
if (this.showFullPrice) {
this.dollars = this.price.toString().split('.')[0]
this.cents = this.price.toString().split('.')[1] || ''
}
if (this.xpay) {
// Format cart link. A normal cart link looks like this:
// https://secure.rosettastone.com/us_en_store_view/checkout/cart/add/sku/90291/category_id/esp/
// We make it look like this:
// https://secure.rosettastone.com/us_en_store_view/checkout/cart/add/sku/90291/category_id/esp/pp/3/
try {
this.productCartLink = this.cartLink.replace(/category_id\/.{3}/, '$&/pp/' + this.xpay)
} catch (e) {
console.warn(`Error converting cartlink for ${this.level}`)
console.log(e)
}
}
/***************************************************************
* Here we will handle the prop values that can be
* overwritten/set by url parameters
***************************************************************/
if (this.$route.query.xpay) {
try {
let xpayObj = JSON.parse(this.$route.query.xpay.replace(/_/g, '"' ))
let xpayValue = xpayObj[this.level]
if (xpayValue) {
this.productCartLink = this.cartLink.replace(/category_id\/.{3}/, '$&/pp/' + xpayValue)
}
} catch (e) {
console.warn('Could not parse xpay parameter.')
console.log(e)
}
}
if (this.$route.query.ribbontext) {
try {
let ribbontextObj = JSON.parse(this.$route.query.ribbontext.replace(/_/g, '"' ))
let textValue = ribbontextObj[this.level]
if (textValue) {
this.productRibbonText = textValue
}
} catch (e) {
console.warn('Could not parse ribbon text parameter.')
console.log(e)
}
}
if (this.$route.query.offertext) {
try {
let offertextObj = JSON.parse(this.$route.query.offertext.replace(/_/g, '"' ))
let textValue = offertextObj[this.level]
if (textValue) {
this.productOfferText = textValue
}
} catch (e) {
console.warn('Could not parse offer text parameter.')
console.log(e)
}
}
},
methods: {
onAddToCartClick(e) {
if (this.iframeCart) {
e.preventDefault()
console.log('---')
}
}
},
computed: {
/**
* If productName is passed a prop, we use it - it means
* it was likely set in Airtable.
* If it was not passed as prop, we use this level as the name,
* in the format `{level}-month subscription. ie, "6-month subscription".
* In such case, we must also format the product name between singular and plural,
* for both US and EN market (month vs months, mes vs meses)
* TODO - store these translations in Airtable somewhere.
*/
productName: {
get () {
// If this product's name was passed via a prop
if (this.name) {
return this.name
}
let translation = {
es: {
singular: 'MES',
plural: 'MESES',
subscription: 'SUBSCRIPTION'
},
en: {
singular: 'MONTH',
plural: 'MONTHS',
subscription: 'SUBSCRIPTION'
},
}
let str
if (parseInt(this.level) == 1) {
str = translation[ this.$store.state.locale ].singular
} else if (parseInt(this.level) > 1) {
str = translation[ this.$store.state.locale ].plural
} else if (isNaN(parseInt(this.level))) {
// this.name is likely "lifetime". use 'subscription', not 'month'
str = ' SUBSCRIPTION'
}
return `${this.level} ${str}`
},
}
}
}
</script>
<style lang='stylus' scoped>
.product-wrap
//flex-basis 238px
//flex-basis 20.78%
width 100%
max-width 238px
position relative
font-family effra
.product
box-shadow 0 8px 30px rgba(0,0,0,.35)
border-radius 7px
background #fff
.ribbontext
background #262626
color #fff
font-weight 700
text-align center
padding 8px 0
position absolute
width 100%
top -29px
left 0
border-top-right-radius 7px
border-top-left-radius 7px
display flex
justify-content center
order 0
.header
order 1
background $yellow
padding 1.66em 0
text-align center
border-top-right-radius 7px
border-top-left-radius 7px
font-weight 700
& > div
text-transform uppercase
font-size 18px
// Special offer per product
.offertext
background #437414
color #fff
//position relative
top -23px
padding 7px 3px
width 85%
font-size 85%
min-height 15px
position absolute
font-weight bold
.offertext-mobile
display none
.body
order 2
text-align center
border-bottom-right-radius 7px
border-bottom-left-radius 7px
display flex
flex-direction column
align-items center
position relative
//box-shadow 0 8px 30px rgba(0,0,0,.3)
padding-top 19px
.pricing
padding 1em 0
display flex
justify-content center
margin-top -20px // same px as we shifted offer-banner up
.monthly
display flex
.dollar
font-weight 700
font-size 72px
// dollar sign $
& > span:first-of-type
position relative
font-size 60%
top -15px
left -1px
.cents
display flex
justify-content center
align-items baseline
position relative
right -3px
flex-direction column
padding 20px 0 0 5px
sup
font-size 19px
top -10px
&:before
content '.'
padding-right 1px
// /month
& > span
font-size 24px
.original-price
font-size 16px
& > span
padding-right 2px
.msrp
color red
text-decoration line-through
.footer
display flex
order 4
a.add-to-cart
background #3f93d3
border 2px dashed #3f93d3
border-radius 40px
text-align center
color #fff
padding 12.5px 76px
text-decoration none
margin 25px auto 1.5em auto
font-weight 700
transition all .14s
position relative
display flex
&:after
content 'BUY'
@media (max-width 1060px)
//.product-wrap
//flex-basis 45%
@media (max-width 668px)
.product-wrap
flex-basis 100%
margin 0
margin-bottom 1em
max-width none
.product
display flex
border-radius 0
box-shadow none
border 1px solid #e3e3e3
justify-content space-between
.ribbontext
position relative
display none
top 0
.header
background #fff
font-size 95%
padding-left 10px
align-self center
& > div
text-align left
.offertext
display none
.offertext-mobile
display block
color #fff
background #01a13a
padding 5px
text-align center
margin-top -1px
.body
display flex
padding 1.8em 0
justify-content center
.pricing
padding 0
margin 0
.monthly
display flex
font-weight 700
font-size 70%
.dollar
padding 0
font-size 2em
& > span:first-of-type
position initial
font-size inherit
sup
position relative
font-size 15px
left -6px
&:after
content '.'
position relative
.cents
font-size 2em
padding 0
position relative
left 0px
top -2px
flex-direction row
text-transform lowercase
sup
top 1px
font-size 13px
&:before
content ''
padding-right 0
& > span
font-size 0.7em
bottom -7px
position relative
.original-price
font-size .7em
padding-top 0.5em
color #555
.price
font-weight bold
.footer
a.add-to-cart
padding 0 13px
border-radius 0
font-size 90%
margin 0
align-items center
box-sizing border-box
&:after
content 'BUY'
</style>
<style lang='stylus'>
.cart-iframe-wrap
position fixed
top 0
bottom 0
right 0
min-height 100%
background #fff
width 573px
border 1px solid red
z-index 5
</style>