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