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