package code
package lib
import model.Item
import net.liftweb._
import util._
/**
* The shopping cart
*/
class Cart {
/**
* The contents of the cart
*/
val contents = ValueCell[Vector[CartItem]](Vector())
/**
* The subtotal
*/
val subtotal = contents.lift(_.foldLeft(zero)(_ +
_.qMult(_.price)))
/**
* The taxable subtotal
*/
val taxableSubtotal = contents.lift(_.filter(_.taxable).
foldLeft(zero)(_ +
_.qMult(_.price)))
/**
* The current tax rate
*/
val taxRate = ValueCell(BigDecimal("0.07"))
/**
* The computed tax
*/
val tax = taxableSubtotal.lift(taxRate)(_ * _)
/**
* The total
*/
val total = subtotal.lift(tax)(_ + _)
/**
* The weight of the cart
*/
val weight = contents.lift(_.foldLeft(zero)(_ +
_.qMult(_.weightInGrams)))
// Helper methods
/**
* A nice constant zero
*/
def zero = BigDecimal(0)
/**
* Add an item to the cart. If it's already in the cart,
* then increment the quantity
*/
def addItem(item: Item) {
contents.atomicUpdate(v => v.find(_.item == item) match {
case Some(ci) => v.map(ci => ci.copy(qnty = ci.qnty +
(if (ci.item == item) 1 else 0)))
case _ => v :+ CartItem(item, 1)
})
}
/**
* Set the item quantity. If zero or negative, remove
*/
def setItemCnt(item: Item, qnty: Int) {
if (qnty <= 0) removeItem(item)
else contents.atomicUpdate(v => v.find(_.item == item) match {
case Some(ci) => v.map(ci => ci.copy(qnty =
(if (ci.item == item) qnty
else ci.qnty)))
case _ => v :+ CartItem(item, qnty)
})
}
/**
* Removes an item from the cart
*/
def removeItem(item: Item) {
contents.atomicUpdate(_.filterNot(_.item == item))
}
}
/**
* An item in the cart
*/
case class CartItem(item: Item, qnty: Int,
id: String = Helpers.nextFuncName) {
/**
* Multiply the quantity times some calculation on the
* contained Item (e.g., getting its weight)
*/
def qMult(f: Item => BigDecimal): BigDecimal = f(item) * qnty
}
/**
* The CartItem companion object
*/
object CartItem {
implicit def cartItemToItem(in: CartItem): Item = in.item
}
package code
package snippet
import model.Item
import comet._
import net.liftweb._
import http._
import sitemap._
import util._
import Helpers._
object AllItemsPage {
// define the menu item for the page that
// will display all items
lazy val menu = Menu.i("Items") / "item" >>
Loc.Snippet("Items", render)
// display the items
def render =
"tbody *" #> renderItems(Item.inventoryItems)
// for a list of items, display those items
def renderItems(in: Seq[Item]) =
"tr" #> in.map(item => {
"a *" #> item.name &
"a [href]" #> AnItemPage.menu.calcHref(item) &
"@description *" #> item.description &
"@price *" #> item.price.toString &
"@add_to_cart [onclick]" #>
SHtml.ajaxInvoke(() => TheCart.addItem(item))})
}
lazy val menu = Menu.i("Items") / "item" >>
Loc.Snippet("Items", render)
<table class="lift:Items">
<tbody>
<tr>
<td name="name"><a href="#">Name</a></td>
<td name="description">Desc</td>
<td name="price">$50.00</td>
<td><button name="add_to_cart">Add to Cart</button></td>
</tr>
</tbody>
</table>
package code
package snippet
import model.Item
import comet._
import net.liftweb._
import util._
import Helpers._
import http._
import sitemap._
import scala.xml.Text
object AnItemPage {
// create a parameterized page
def menu = Menu.param[Item]("Item", Loc.LinkText(i => Text(i.name)),
Item.find _, _.id) / "item" / *
}
class AnItemPage(item: Item) {
def render = "@name *" #> item.name &
"@description *" #> item.description &
"@price *" #> item.price.toString &
"@add_to_cart [onclick]" #> SHtml.ajaxInvoke(() => TheCart.addItem(item))
}
def menu = Menu.param[Item]("Item", Loc.LinkText(i => Text(i.name)),
Item.find _, _.id) / "item" / *
package code
package comet
import lib._
import net.liftweb._
import common._
import http._
import util._
import js._
import js.jquery._
import JsCmds._
import scala.xml.NodeSeq
import Helpers._
/**
* What's the current cart for this session
*/
object TheCart extends SessionVar(new Cart())
/**
* The CometCart is the CometActor the represents the shopping cart
*/
class CometCart extends CometActor {
// our current cart
private var cart = TheCart.get
/**
* Draw yourself
*/
def render = {
"#contents" #> (
"tbody" #>
Helpers.findOrCreateId(id => // make sure tbody has an id
// when the cart contents updates
WiringUI.history(cart.contents) {
(old, nw, ns) => {
// capture the tr part of the template
val theTR = ("tr ^^" #> "**")(ns)
def ciToId(ci: CartItem): String = ci.id + "_" + ci.qnty
// build a row out of a cart item
def html(ci: CartItem): NodeSeq = {
("tr [id]" #> ciToId(ci) &
"@name *" #> ci.name &
"@qnty *" #> SHtml.
ajaxText(ci.qnty.toString,
s => {
TheCart.
setItemCnt(ci,
Helpers.toInt(s))
}, "style" -> "width: 20px;") &
"@del [onclick]" #> SHtml.
ajaxInvoke(() => TheCart.removeItem(ci)))(theTR)
}
// calculate the delta between the lists and
// based on the deltas, emit the current jQuery
// stuff to update the display
JqWiringSupport.calculateDeltas(old, nw, id)(ciToId _, html _)
}
})) &
"#subtotal" #> WiringUI.asText(cart.subtotal) & // display the subttotal
"#tax" #> WiringUI.asText(cart.tax) & // display the tax
"#total" #> WiringUI.asText(cart.total) // display the total
}
/**
* Process messages from external sources
*/
override def lowPriority = {
// if someone sends us a new cart
case SetNewCart(newCart) => {
// unregister from the old cart
unregisterFromAllDepenencies()
// remove all the dependencies for the old cart
// from the postPageJavaScript
theSession.clearPostPageJavaScriptForThisPage()
// set the new cart
cart = newCart
// do a full reRender including the fixed render piece
reRender(true)
}
}
}
/**
* Set a new cart for the CometCart
*/
case class SetNewCart(cart: Cart)
object TheCart extends SessionVar(new Cart())
class CometCart extends CometActor {
// our current cart
private var cart = TheCart.get
"#total" #> WiringUI.asText(cart.total) // display the total
"#contents" #> (
"tbody" #>
Helpers.findOrCreateId(id => // make sure tbody has an id
// when the cart contents updates
WiringUI.history(cart.contents) {
(old, nw, ns) => {
// capture the tr part of the template
val theTR = ("tr ^^" #> "**")(ns)
def ciToId(ci: CartItem): String = ci.id + "_" + ci.qnty
// build a row out of a cart item
def html(ci: CartItem): NodeSeq = {
("tr [id]" #> ciToId(ci) &
"@name *" #> ci.name &
"@qnty *" #> SHtml.
ajaxText(ci.qnty.toString,
s => {
TheCart.
setItemCnt(ci,
Helpers.toInt(s))
}, "style" -> "width: 20px;") &
"@del [onclick]" #> SHtml.
ajaxInvoke(() => TheCart.removeItem(ci)))(theTR)
}
// calculate the delta between the lists and
// based on the deltas, emit the current jQuery
// stuff to update the display
JqWiringSupport.calculateDeltas(old, nw, id)(ciToId _, html _)
}
}))
// if someone sends us a new cart
case SetNewCart(newCart) => {
// unregister from the old cart
unregisterFromAllDepenencies()
// remove all the dependencies for the old cart
// from the postPageJavaScript
theSession.clearPostPageJavaScriptForThisPage()
// set the new cart
cart = newCart
// do a full reRender including the fixed render piece
reRender(true)
}
package code
package snippet
import model._
import comet._
import lib._
import net.liftweb._
import http._
import util.Helpers._
import js._
import JsCmds._
import js.jquery.JqJsCmds._
class Link {
// open a modal dialog based on the _share_link.html template
def request = "* [onclick]" #> SHtml.ajaxInvoke(() => {
(for {
template <- TemplateFinder.findAnyTemplate(List("_share_link"))
} yield ModalDialog(template)) openOr Noop
})
// close the modal dialog
def close = "* [onclick]" #> SHtml.ajaxInvoke(() => Unblock)
// Generate the href and link for sharing
def generate = {
val s = ShareCart.generateLink(TheCart)
"a [href]" #> s & "a *" #> s
}
}
package code
package lib
import comet._
import net.liftweb._
import common._
import http._
import rest.RestHelper
import util._
import Helpers._
// it's a RestHelper
object ShareCart extends RestHelper {
// private state
private var carts: Map[String, (Long, Cart)] = Map()
// given a Cart, generate a unique sharing code
def codeForCart(cart: Cart): String = synchronized {
val ret = Helpers.randomString(12)
carts += ret -> (10.minutes.later.millis -> cart)
ret
}
/**
* Generate the right link to this cart
*/
def generateLink(cart: Cart): String = {
S.hostAndPath + "/co_shop/"+codeForCart(cart)
}
// An extractor that converts a String to a Cart, if
// possible
def unapply(code: String): Option[Cart] = synchronized {
carts.get(code).map(_._2)
}
// remove any carts that are 10+ minutes old
private def cleanup() {
val now = Helpers.millis
synchronized{
carts = carts.filter{
case (_, (time, _)) => time > now
}
}
Schedule.schedule(() => cleanup(), 5 seconds)
}
// clean up every 5 seconds
cleanup()
// the REST part of the code
serve {
// match the incoming URL
case "co_shop" :: ShareCart(cart) :: Nil Get _ => {
// set the cart
TheCart.set(cart)
// send the SetNewCart message to the CometCart
S.session.foreach(
_.sendCometActorMessage("CometCart", Empty,
SetNewCart(cart)))
// redirect the browser to /
RedirectResponse("/")
}
}
}
(C) 2012 David Pollak