mark: A photo of Mark kneeling on top of the Taal Volcano in the Philippines. It was a long hike. (Default)
Mark Smith ([staff profile] mark) wrote in [site community profile] changelog2009-04-14 08:07 am

[dw-free] Finishing up payment system

[commit: http://hg.dwscoalition.org/dw-free/rev/b599f8330c87]

http://bugs.dwscoalition.org/show_bug.cgi?id=116

More work on the shop interface.

Patch by [personal profile] janinedog.

Files modified:
  • bin/upgrading/en.dat
  • cgi-bin/DW/Pay.pm
  • cgi-bin/DW/Shop/Cart.pm
  • cgi-bin/DW/Shop/Item/Account.pm
  • cgi-bin/DW/Widget/PaidAccountStatus.pm
  • cgi-bin/DW/Widget/ShopCartStatusBar.pm
  • cgi-bin/DW/Widget/ShopItemGroupDisplay.pm
  • cgi-bin/LJ/Widget/ShopCart.pm
  • cgi-bin/LJ/Widget/ShopItemOptions.pm
  • htdocs/shop.bml
  • htdocs/shop.bml.text
  • htdocs/shop/account.bml
  • htdocs/shop/account.bml.text
  • htdocs/shop/cart.bml
  • htdocs/shop/cart.bml.text
  • htdocs/shop/index.bml
  • htdocs/stc/shop.css
  • htdocs/stc/widgets/shop.css
--------------------------------------------------------------------------------
diff -r 2530f9f773f5 -r b599f8330c87 bin/upgrading/en.dat
--- a/bin/upgrading/en.dat	Tue Apr 14 07:34:07 2009 +0000
+++ b/bin/upgrading/en.dat	Tue Apr 14 08:07:41 2009 +0000
@@ -2946,6 +2946,14 @@ settings.yearofbirth=Year of Birth
 
 settings.zipcode=ZIP Code
 
+shop.item.account.canbeadded.noperms=There are no more seed accounts available for purchase at this time.
+
+shop.item.account.conflicts.differentpaid=You cannot purchase two different types of paid accounts for the same person.
+
+shop.item.account.conflicts.multipleperms=You cannot purchase more than one seed account for the same person.
+
+shop.item.account.name=[[name]] ([[num]] [[?num|month|months]])
+
 sitescheme.accountlinks.account=Account
 
 sitescheme.accountlinks.btn.login=Log in
@@ -3855,6 +3863,10 @@ widget.navstripchooser.upgradetos2=<a [[
 
 widget.pagenotice.dismiss=Dismiss
 
+widget.paidaccountstatus.accounttype=Your current account type is:
+
+widget.paidaccountstatus.expiretime=Your paid time expires:
+
 widget.popularinterests.viewall=view all popular interests
 
 widget.qotd.answer=Answer
@@ -3966,6 +3978,76 @@ widget.search.username=Username
 widget.search.username=Username
 
 widget.search.yahoo=Yahoo! ID
+
+widget.shopcart.anonymous=Anonymous
+
+widget.shopcart.btn.checkout=Check Out
+
+widget.shopcart.btn.discard=Discard Entire Cart
+
+widget.shopcart.btn.removeselected=Remove Selected Items
+
+widget.shopcart.deliverydate.today=Today
+
+widget.shopcart.error.nocart=Unable to get a shopping cart for you.  Please try again later.
+
+widget.shopcart.error.noitems=You have no items in your shopping cart.
+
+widget.shopcart.header.deliverydate=Delivery Date
+
+widget.shopcart.header.from=From
+
+widget.shopcart.header.item=Item
+
+widget.shopcart.header.price=Price
+
+widget.shopcart.header.remove=Remove?
+
+widget.shopcart.header.to=To
+
+widget.shopcart.paymentmethod=Payment Method:
+
+widget.shopcart.paymentmethod.checkmoneyorder=Check/Money Order
+
+widget.shopcart.paymentmethod.paypal=PayPal
+
+widget.shopcart.total=Total:
+
+widget.shopcartstatusbar.header=Shopping Cart
+
+widget.shopcartstatusbar.itemcount=[[num]] [[?num|item|items]] for a total of [[price]]
+
+widget.shopcartstatusbar.newcart=Discard Cart
+
+widget.shopcartstatusbar.viewcart=View Cart
+
+widget.shopitemgroupdisplay.paidaccounts.header=Buy a Paid Account
+
+widget.shopitemgroupdisplay.paidaccounts.item.differentaccount=<a [[aopts]]>For a different existing account</a>
+
+widget.shopitemgroupdisplay.paidaccounts.item.exisitingaccount=<a [[aopts]]>For an existing account</a>
+
+widget.shopitemgroupdisplay.paidaccounts.item.newaccount=<a [[aopts]]>For a new account</a>
+
+widget.shopitemgroupdisplay.paidaccounts.item.self=<a [[aopts]]>For yourself</a> ([[user]])
+
+widget.shopitemoptions.error.invalidusername=The username you entered is invalid or the user does not exist.
+
+widget.shopitemoptions.error.nocart=Unable to get a shopping cart for you.  Please try again later.
+
+widget.shopitemoptions.error.notloggedin=You must be logged in as a personal account in order to purchase paid time for yourself.
+
+widget.shopitemoptions.header.paid=Paid Account
+
+widget.shopitemoptions.header.prem=Premium Paid Account
+
+widget.shopitemoptions.header.seed=Seed Account
+
+widget.shopitemoptions.highlight.seed=- Fewer than [[num]] remaining!
+
+widget.shopitemoptions.price=[[num]] [[?num|month|months]] for [[price]]
+
+widget.shopitemoptions.price.seed=Forever for [[price]]
 
 widget.support.submit.button=Submit Request
 
diff -r 2530f9f773f5 -r b599f8330c87 cgi-bin/DW/Pay.pm
--- a/cgi-bin/DW/Pay.pm	Tue Apr 14 07:34:07 2009 +0000
+++ b/cgi-bin/DW/Pay.pm	Tue Apr 14 08:07:41 2009 +0000
@@ -579,6 +579,52 @@ sub update_paid_status {
     return 1;
 }
 
+################################################################################
+# DW::Pay::num_permanent_accounts_available
+#
+# ARGUMENTS: none
+#
+# RETURN: number of permanent accounts that are still available for purchase
+#         -1 if there is no limit on how many permanent accounts can be
+#         purchased
+#
+sub num_permanent_accounts_available {
+    return 0 unless $LJ::PERMANENT_ACCOUNT_LIMIT;
+    return -1 if $LJ::PERMANENT_ACCOUNT_LIMIT < 0;
+
+    # FIXME: we need to figure out the best way to do this, which is probably
+    # to have some counter that is incremented (or decremented) whenever someone
+    # finishes the check out process with a permanent account in their cart
+    my $num_bought = 0;
+    my $num_available = $LJ::PERMANENT_ACCOUNT_LIMIT - $num_bought;
+
+    return $num_available > 0 ? $num_available : 0;
+}
+
+################################################################################
+# DW::Pay::num_permanent_accounts_available_estimated
+#
+# ARGUMENTS: none
+#
+# RETURN: estimated number of permanent accounts that are still available for
+#         purchase
+#         -1 if there is no limit on how many permanent accounts can be
+#         purchased
+#
+sub num_permanent_accounts_available_estimated {
+    my $num_available = DW::Pay::num_permanent_accounts_available();
+    return $num_available if $num_available < 1;
+
+    return 10  if $num_available <= 10;
+    return 25  if $num_available <= 25;
+    return 50  if $num_available <= 50;
+    return 100 if $num_available <= 100;
+    return 150 if $num_available <= 150;
+    return 200 if $num_available <= 200;
+    return 300 if $num_available <= 300;
+    return 400 if $num_available <= 400;
+    return 500;
+}
 
 ################################################################################
 ################################################################################
diff -r 2530f9f773f5 -r b599f8330c87 cgi-bin/DW/Shop/Cart.pm
--- a/cgi-bin/DW/Shop/Cart.pm	Tue Apr 14 07:34:07 2009 +0000
+++ b/cgi-bin/DW/Shop/Cart.pm	Tue Apr 14 08:07:41 2009 +0000
@@ -143,17 +143,31 @@ sub save {
 }
 
 
+# returns the number of items in this cart
+sub num_items {
+    my $self = $_[0];
+
+    return scalar @{ $self->{items} || [] };
+}
+
+
 # returns 1/0 if this cart has any items in it
 sub has_items {
     my $self = $_[0];
 
-    return scalar( @{ $self->{items} || [] } ) > 0 ? 1 : 0;
+    return $self->num_items > 0 ? 1 : 0;
 }
 
 
 # add an item to the shopping cart, returns 1/0
 sub add_item {
     my ( $self, $item ) = @_;
+
+    # make sure this item is allowed to be added
+    my $error;
+    unless ( $item->can_be_added( errref => \$error ) ) {
+        return ( 0, $error );
+    }
 
     # iterate over existing items to see if any conflict
     foreach my $it ( @{$self->items} ) {
diff -r 2530f9f773f5 -r b599f8330c87 cgi-bin/DW/Shop/Item/Account.pm
--- a/cgi-bin/DW/Shop/Item/Account.pm	Tue Apr 14 07:34:07 2009 +0000
+++ b/cgi-bin/DW/Shop/Item/Account.pm	Tue Apr 14 08:07:41 2009 +0000
@@ -6,6 +6,7 @@
 #
 # Authors:
 #      Mark Smith <mark@dreamwidth.org>
+#      Janine Costanzo <janine@netrophic.com>
 #
 # Copyright (c) 2009 by Dreamwidth Studios, LLC.
 #
@@ -26,29 +27,30 @@ sub new {
     my $type = delete $args{type};
     return undef unless exists $LJ::SHOP{$type};
 
-    # at this point, there needs to be only one argument, and it needs to be one
-    # of the target types
-    return undef unless
-        scalar( keys %args ) == 1 &&
-            ( $args{target_username} || $args{target_userid} || $args{target_email} );
+    # from_userid will be 0 if the sender isn't logged in
+    return undef unless $args{from_userid} == 0 || LJ::load_userid( $args{from_userid} );
 
     # now do validation.  since new is only called when the item is being added
     # to the shopping cart, then we are comfortable doing all of these checks
     # on things at the time this item is put together
-    if ( my $un = $args{target_username} ) {
-        # username needs to be valid and not exist
-        return undef unless $un = LJ::canonical_username( $un );
-        return undef if LJ::load_user( $un );
-
-        $args{target_username} = $un;
-
-    } elsif ( my $uid = $args{target_userid} ) {
+    if ( my $uid = $args{target_userid} ) {
         # userid needs to exist
         return undef unless LJ::load_userid( $uid );
+    } elsif ( my $email = $args{target_email} ) {
+        # email address must be valid
+        my @email_errors;
+        LJ::check_email( $email, \@email_errors );
+        return undef if @email_errors;
+    } else {
+        return undef;
+    }
 
-    } elsif ( my $email = $args{target_email} ) {
-        # FIXME: validate email address
+    if ( $args{deliverydate} ) {
+        return undef unless $args{deliverydate} =~ /^\d\d\d\d-\d\d-\d\d$/;
+    }
 
+    if ( $args{anonymous} ) {
+        return undef unless $args{anonymous} == 1;
     }
 
     # looks good
@@ -93,6 +95,22 @@ sub unapply {
 }
 
 
+# returns 1 if this item is allowed to be added to the shopping cart
+sub can_be_added {
+    my ( $self, %opts ) = @_;
+
+    my $errref = $opts{errref};
+
+    # check to see if we're over the permanent account limit
+    if ( $self->permanent && DW::Pay::num_permanent_accounts_available() < 1 ) {
+        $$errref = LJ::Lang::ml( 'shop.item.account.canbeadded.noperms' );
+        return 0;
+    }
+
+    return 1;
+}
+
+
 # given another item, see if that item conflicts with this item (i.e.,
 # if you can't have both in your shopping cart at the same time).
 #
@@ -101,20 +119,20 @@ sub conflicts {
     my ( $self, $item ) = @_;
 
     # first see if we're talking about the same target
-    # FIXME: maybe not include email here, what happens if they want to buy 3 paid accounts
-    # and send them to the same email address?
+    # note that we're not checking email here because they may want to buy
+    # multiple paid accounts and send them to all to the same email address
+    # (so they can can create multiple new paid accounts)
     return if
-        ( $self->t_userid   && $self->t_userid   != $item->t_userid   ) ||
-        ( $self->t_email    && $self->t_email    != $item->t_email    ) ||
-        ( $self->t_username && $self->t_username != $item->t_username );
+        ( $self->t_userid && ( $self->t_userid != $item->t_userid ) ) ||
+        ( $self->t_email                                            );
 
-    # target same, if either is permanent, then fail because
+    # target same, if both are permanent, then fail because
     # THERE CAN BE ONLY ONE
-    return 'Already purchasing a permanent account for this target.'
-        if $self->permanent || $item->permanent;
+    return LJ::Lang::ml( 'shop.item.account.conflicts.multipleperms' )
+        if $self->permanent && $item->permanent;
 
     # otherwise ensure that the classes are the same
-    return 'Already chose to upgrade to a ' . $self->class . ', do not do both!'
+    return LJ::Lang::ml( 'shop.item.account.conflicts.differentpaid' )
         if $self->class ne $item->class;
 
     # guess we allow it
@@ -132,12 +150,6 @@ sub t_html {
             if $u;
         return "<strong>invalid userid $uid</strong>";
 
-    } elsif ( my $user = $self->t_username ) {
-        my $u = LJ::load_user( $user );
-        return $u->ljuser_display
-            if $u;
-        return "<strong>$user</strong>";
-
     } elsif ( my $email = $self->t_email ) {
         return "<strong>$email</strong>";
 
@@ -147,6 +159,23 @@ sub t_html {
 }
 
 
+# render the item name as a string
+sub name_html {
+    my $self = $_[0];
+
+    my $name = "invalid name";
+    foreach my $cap ( keys %LJ::CAP ) {
+        if ( $LJ::CAP{$cap} && $LJ::CAP{$cap}->{_account_type} eq $self->class ) {
+            $name = $LJ::CAP{$cap}->{_visible_name};
+            last;
+        }
+    }
+
+    return $name if $self->permanent;
+    return LJ::Lang::ml( 'shop.item.account.name', { name => $name, num => $self->months } );
+}
+
+
 # this is a getter/setter so it is pulled out
 sub id {
     return $_[0]->{id} unless defined $_[1];
@@ -155,14 +184,16 @@ sub id {
 
 
 # simple accessors
-sub applied    { return $_[0]->{applied};         }
-sub cost       { return $_[0]->{cost};            }
-sub months     { return $_[0]->{months};          }
-sub class      { return $_[0]->{class};           }
-sub t_userid   { return $_[0]->{target_userid};   }
-sub t_email    { return $_[0]->{target_email};    }
-sub t_username { return $_[0]->{target_username}; }
-sub permanent  { return $_[0]->months == 99;      }
+sub applied      { return $_[0]->{applied};         }
+sub cost         { return $_[0]->{cost};            }
+sub months       { return $_[0]->{months};          }
+sub class        { return $_[0]->{class};           }
+sub t_userid     { return $_[0]->{target_userid};   }
+sub t_email      { return $_[0]->{target_email};    }
+sub permanent    { return $_[0]->months == 99;      }
+sub from_userid  { return $_[0]->{from_userid};     }
+sub deliverydate { return $_[0]->{deliverydate};    }
+sub anonymous    { return $_[0]->{anonymous};       }
 
 
 1;
diff -r 2530f9f773f5 -r b599f8330c87 cgi-bin/DW/Widget/PaidAccountStatus.pm
--- a/cgi-bin/DW/Widget/PaidAccountStatus.pm	Tue Apr 14 07:34:07 2009 +0000
+++ b/cgi-bin/DW/Widget/PaidAccountStatus.pm	Tue Apr 14 08:07:41 2009 +0000
@@ -6,6 +6,7 @@
 #
 # Authors:
 #      Mark Smith <mark@dreamwidth.org>
+#      Janine Costanzo <janine@netrophic.com>
 #
 # Copyright (c) 2009 by Dreamwidth Studios, LLC.
 #
@@ -23,10 +24,8 @@ use DW::Pay;
 use DW::Pay;
 use DW::Shop;
 
-# general purpose shop CSS used by the entire shop system
-sub need_res { qw( stc/widgets/shop.css ) }
+sub need_res { qw( stc/shop.css ) }
 
-# main renderer for this particular thingy
 sub render_body {
     my ( $class, %opts ) = @_;
 
@@ -36,15 +35,13 @@ sub render_body {
     my $account_type = DW::Pay::get_account_type_name( $remote );
     my $expires_at = DW::Pay::get_account_expiration_time( $remote );
     my $expires_on = $expires_at > 0
-                     ? 'Your paid time expires: ' . LJ::mysql_time( $expires_at )
+                     ? "<br />" . $class->ml( 'widget.paidaccountstatus.expiretime' ) . " " . LJ::mysql_time( $expires_at )
                      : '';
 
-    my $ret = qq{
-<div class='shop-account-status'>
-    Your current account type is: <strong>$account_type</strong><br />
-    $expires_on
-</div>
-    };
+    my $ret = "<div class='shop-account-status'>";
+    $ret .= $class->ml( 'widget.paidaccountstatus.accounttype' ) . " ";
+    $ret .= "<strong>$account_type</strong>$expires_on";
+    $ret .= "</div>";
 
     return $ret;
 }
diff -r 2530f9f773f5 -r b599f8330c87 cgi-bin/DW/Widget/ShopCartStatusBar.pm
--- a/cgi-bin/DW/Widget/ShopCartStatusBar.pm	Tue Apr 14 07:34:07 2009 +0000
+++ b/cgi-bin/DW/Widget/ShopCartStatusBar.pm	Tue Apr 14 08:07:41 2009 +0000
@@ -6,6 +6,7 @@
 #
 # Authors:
 #      Mark Smith <mark@dreamwidth.org>
+#      Janine Costanzo <janine@netrophic.com>
 #
 # Copyright (c) 2009 by Dreamwidth Studios, LLC.
 #
@@ -22,10 +23,8 @@ use Carp qw/ croak /;
 
 use DW::Shop;
 
-# general purpose shop CSS used by the entire shop system
-sub need_res { qw( stc/widgets/shop.css ) }
+sub need_res { qw( stc/shop.css ) }
 
-# main renderer for this particular thingy
 sub render_body {
     my ( $class, %opts ) = @_;
 
@@ -37,19 +36,21 @@ sub render_body {
     # old cart is gone with the wind ...
     my $cart = $opts{newcart} ? DW::Shop::Cart->new_cart( $u ) : $shop->cart;
 
-    # if minimal, and the cart is empty, bail
-    return if $opts{minimal} && ! $cart->has_items;
+    # if the cart is empty, bail
+    return unless $cart->has_items;
 
     # render out information about this cart
-    my $ret = '[ ';
-    $ret .= 'Shopping Cart for ' . ( $u ? $u->ljuser_display : 'anonymous user' );
-    $ret .= '; cartid = ' . $cart->id;
-    $ret .= ' created ' . LJ::ago_text( $cart->age );
-    $ret .= '; total = $' . $cart->display_total;
-    $ret .= '; <a href="/shop?newcart=1">make new cart</a>';
-    $ret .= '; <a href="/shop/cart">view cart</a>';
-    $ret .= '; <a href="/shop/checkout">checkout</a>';
-    $ret .= ' ]';
+    my $ret = "<div class='shop-cart-status'>";
+    $ret .= "<strong>" . $class->ml( 'widget.shopcartstatusbar.header' ) . "</strong><br />";
+    $ret .= $class->ml( 'widget.shopcartstatusbar.itemcount', { num => $cart->num_items, price => '$' . $cart->display_total . " USD" } );
+    $ret .= "<br />";
+
+    $ret .= "<ul>";
+    $ret .= "<li><a href='$LJ::SITEROOT/shop/cart'><strong>" . $class->ml( 'widget.shopcartstatusbar.viewcart' ) . "</strong></a></li>";
+    $ret .= "<li><a href='$LJ::SITEROOT/shop?newcart=1'><strong>" . $class->ml( 'widget.shopcartstatusbar.newcart' ) . "</strong></a></li>";
+    $ret .= "</ul>";
+
+    $ret .= "</div>";
 
     return $ret;
 }
diff -r 2530f9f773f5 -r b599f8330c87 cgi-bin/DW/Widget/ShopItemGroupDisplay.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Widget/ShopItemGroupDisplay.pm	Tue Apr 14 08:07:41 2009 +0000
@@ -0,0 +1,47 @@
+#!/usr/bin/perl
+#
+# DW::Widget::ShopItemGroupDisplay
+#
+# Renders a group of shop items for display on the first page of the shop.
+#
+# Authors:
+#      Janine Costanzo <janine@netrophic.com>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself.  For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+package DW::Widget::ShopItemGroupDisplay;
+
+use strict;
+use base qw/ LJ::Widget /;
+use Carp qw/ croak /;
+
+sub need_res { qw( stc/shop.css ) }
+
+sub render_body {
+    my ( $class, %opts ) = @_;
+
+    my $remote = LJ::get_remote();
+    my $ret;
+
+    if ( $opts{group} eq 'paidaccounts' ) {
+        $ret .= "<h2>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.header' ) . "</h2>";
+        $ret .= "<ul>";
+        if ( $remote && $remote->is_personal ) {
+            $ret .= "<li>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.item.self', { aopts => "href='$LJ::SITEROOT/shop/account?for=self'", user => $remote->ljuser_display } ) . "</li>";
+            $ret .= "<li>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.item.differentaccount', { aopts => "href='$LJ::SITEROOT/shop/account?for=gift'" } ) . "</li>";
+        } else {
+            $ret .= "<li>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.item.exisitingaccount', { aopts => "href='$LJ::SITEROOT/shop/account?for=gift'" } ) . "</li>";
+        }
+        $ret .= "<li>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.item.newaccount', { aopts => "href='$LJ::SITEROOT/shop/account?for=new'" } ) . "</li>";
+        $ret .= "</ul>";
+    }
+
+    return $ret;
+}
+
+1;
diff -r 2530f9f773f5 -r b599f8330c87 cgi-bin/LJ/Widget/ShopCart.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/LJ/Widget/ShopCart.pm	Tue Apr 14 08:07:41 2009 +0000
@@ -0,0 +1,115 @@
+#!/usr/bin/perl
+#
+# LJ::Widget::ShopCart
+#
+# Returns the current shopping cart for the remote user.
+#
+# Authors:
+#      Janine Costanzo <janine@netrophic.com>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself.  For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+package LJ::Widget::ShopCart;
+
+use strict;
+use base qw/ LJ::Widget /;
+use Carp qw/ croak /;
+
+sub need_res { qw( stc/shop.css ) }
+
+sub render_body {
+    my ( $class, %opts ) = @_;
+
+    my $ret;
+
+    my $cart = DW::Shop->get->cart
+        or return $class->ml( 'widget.shopcart.error.nocart' );
+
+    return $class->ml( 'widget.shopcart.error.noitems' )
+        unless @{$cart->items};
+
+    $ret .= $class->start_form;
+
+    $ret .= "<table class='shop-cart'>";
+    $ret .= "<tr><th>" . $class->ml( 'widget.shopcart.header.remove' ) . "</th>";
+    $ret .= "<th>" . $class->ml( 'widget.shopcart.header.item' ) . "</th>";
+    $ret .= "<th>" . $class->ml( 'widget.shopcart.header.deliverydate' ) . "</th>";
+    $ret .= "<th>" . $class->ml( 'widget.shopcart.header.to' ) . "</th>";
+    $ret .= "<th>" . $class->ml( 'widget.shopcart.header.from' ) . "</th>";
+    $ret .= "<th>" . $class->ml( 'widget.shopcart.header.price' ) . "</th></tr>";
+    foreach my $item ( @{$cart->items} ) {
+        my $from_u = LJ::load_userid( $item->from_userid );
+
+        $ret .= "<tr>";
+        $ret .= "<td>" . $class->html_check( name => 'remove_' . $item->id, value => 1 ) . "</td>";
+        $ret .= "<td>" . $item->name_html . "</td>";
+        $ret .= "<td>" . ( $item->deliverydate ? $item->deliverydate : $class->ml( 'widget.shopcart.deliverydate.today' ) ) . "</td>";
+        $ret .= "<td>" . $item->t_html . "</td>";
+        $ret .= "<td>" . ( $item->anonymous || !LJ::isu( $from_u ) ? $class->ml( 'widget.shopcart.anonymous' ) : $from_u->ljuser_display ) . "</td>";
+        $ret .= "<td>\$" . $item->cost . " USD</td>";
+        $ret .= "</tr>";
+    }
+    $ret .= "<tr><td colspan='6' class='total'>" . $class->ml( 'widget.shopcart.total' ) . " \$" . $cart->display_total . " USD</td></tr>";
+    $ret .= "</table>";
+
+    $ret .= "<div class='shop-cart-btn'>";
+
+    $ret .= "<p>" . $class->html_submit( removeselected => $class->ml( 'widget.shopcart.btn.removeselected' ) ) . " ";
+    $ret .= $class->html_submit( discard => $class->ml( 'widget.shopcart.btn.discard' ) ) . "</p>";
+
+    my @paypal_option = ( paypal => $class->ml( 'widget.shopcart.paymentmethod.paypal' ) )
+        if keys %LJ::PAYPAL_CONFIG;
+    $ret .= "<p>" . $class->ml( 'widget.shopcart.paymentmethod' ) . " ";
+    $ret .= $class->html_select(
+        name => 'paymentmethod',
+        selected => keys %LJ::PAYPAL_CONFIG ? 'paypal' : 'checkmoneyorder',
+        list => [
+            @paypal_option,
+            checkmoneyorder => $class->ml( 'widget.shopcart.paymentmethod.checkmoneyorder' ),
+        ],
+    ) . " ";
+    $ret .= $class->html_submit( checkout => $class->ml( 'widget.shopcart.btn.checkout' ) ) . "</p>";
+
+    $ret .= "</div>";
+
+    $ret .= $class->end_form;
+
+    return $ret;
+}
+
+sub handle_post {
+    my ( $class, $post, %opts ) = @_;
+
+    # check out
+    if ( $post->{checkout} ) {
+        my $method = 'paypal';
+        $method = 'checkmoneyorder' if $post->{paymentmethod} eq 'checkmoneyorder' || !keys %LJ::PAYPAL_CONFIG;
+
+        return BML::redirect( "$LJ::SITEROOT/shop/checkout?method=$method" );
+    }
+
+    # remove selected items
+    if ( $post->{removeselected} ) {
+        my $cart = DW::Shop->get->cart
+            or return ( error => $class->ml( 'widget.shopcart.error.nocart' ) );
+
+        foreach my $val ( keys %$post ) {
+            next unless $post->{$val} && $val =~ /^remove_(\d+)$/;
+            $cart->remove_item( $1 );
+        }
+    }
+
+    # discard entire cart
+    if ( $post->{discard} ) {
+        return BML::redirect( "$LJ::SITEROOT/shop?newcart=1" );
+    }
+
+    return;
+}
+
+1;
diff -r 2530f9f773f5 -r b599f8330c87 cgi-bin/LJ/Widget/ShopItemOptions.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/LJ/Widget/ShopItemOptions.pm	Tue Apr 14 08:07:41 2009 +0000
@@ -0,0 +1,137 @@
+#!/usr/bin/perl
+#
+# LJ::Widget::ShopItemOptions
+#
+# Returns the options for purchasing a particular shop item.
+#
+# Authors:
+#      Janine Costanzo <janine@netrophic.com>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself.  For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+package LJ::Widget::ShopItemOptions;
+
+use strict;
+use base qw/ LJ::Widget /;
+use Carp qw/ croak /;
+
+sub need_res { qw( stc/shop.css ) }
+
+sub render_body {
+    my ( $class, %opts ) = @_;
+
+    my $remote = LJ::get_remote();
+    my $ret;
+
+    my $option_name = $opts{option_name};
+    my $given_item = $opts{item};
+
+    return "" unless $option_name && $given_item;
+
+    # get all of the possible month values for the item
+    # note that it's okay if there's no month values for an item;
+    # we'll just print the item itself in that case
+    my @month_values;
+    foreach my $item ( keys %LJ::SHOP ) {
+        if ( $item =~ /^$given_item(\d*)$/ ) {
+            push @month_values, $1;
+        }
+    }
+
+    $ret .= "<strong>" . $class->ml( "widget.shopitemoptions.header.$given_item" ) . "</strong>";
+
+    my $num_perms = DW::Pay::num_permanent_accounts_available_estimated();
+    if ( $num_perms > 0 ) {
+        my $highlight_string = $class->ml( "widget.shopitemoptions.highlight.$given_item", { num => $num_perms } );
+        $ret .= " <span class='shop-item-highlight'>$highlight_string</span>"
+            unless $highlight_string eq 'ShopItemOptions';
+    }
+
+    $ret .= "<br />";
+
+    foreach my $month_value ( sort { $a <=> $b } @month_values ) {
+        my $full_item = $given_item . $month_value;
+        if ( ref $LJ::SHOP{$full_item} eq 'ARRAY' ) {
+            my $price_string = $class->ml( "widget.shopitemoptions.price.$full_item", { price => "\$$LJ::SHOP{$full_item}->[0] USD" } );
+            $price_string = $class->ml( 'widget.shopitemoptions.price', { num => $month_value, price => "\$$LJ::SHOP{$full_item}->[0] USD" } )
+                if $price_string eq 'ShopItemOptions';
+
+            $ret .= $class->html_check(
+                type => 'radio',
+                name => $option_name,
+                id => $full_item,
+                value => $full_item,
+            ) . " <label for='$full_item'>$price_string</label><br />";
+        }
+    }
+
+    return $ret;
+}
+
+sub handle_post {
+    my ( $class, $post, %opts ) = @_;
+
+    # now try to add this item to their list
+    my $cart = DW::Shop->get->cart
+        or return ( error => $class->ml( 'widget.shopitemoptions.error.nocart' ) );
+
+    my %item_data;
+
+    my $remote = LJ::get_remote();
+    $item_data{from_userid} = $remote ? $remote->id : 0;
+
+    if ( $post->{for} eq 'self' ) {
+        if ( $remote && $remote->is_personal ) {
+            $item_data{target_userid} = $remote->id;
+        } else {
+            return ( error => $class->ml( 'widget.shopitemoptions.error.notloggedin' ) );
+        }
+    } elsif ( $post->{for} eq 'gift' ) {
+        my $target_u = LJ::load_user( $post->{username} );
+        if ( LJ::isu( $target_u ) ) {
+            $item_data{target_userid} = $target_u->id;
+        } else {
+            return ( error => $class->ml( 'widget.shopitemoptions.error.invalidusername' ) );
+        }
+    } elsif ( $post->{for} eq 'new' ) {
+        my @email_errors;
+        LJ::check_email( $post->{email}, \@email_errors );
+        if ( @email_errors ) {
+            return ( error => join( ', ', @email_errors ) );
+        } else {
+            $item_data{target_email} = $post->{email};
+        }
+    }
+
+    if ( $post->{deliverydate_mm} && $post->{deliverydate_dd} && $post->{deliverydate_yyyy} ) {
+        my $given_date = DateTime->new(
+            year => $post->{deliverydate_yyyy}+0,
+            month => $post->{deliverydate_mm}+0,
+            day => $post->{deliverydate_dd}+0,
+        );
+
+        $item_data{deliverydate} = $given_date->date
+            unless $given_date->date eq DateTime->today->date;
+    }
+
+    $item_data{anonymous} = 1
+        if $post->{anonymous} || !$remote;
+
+    # build a new item and try to toss it in the cart.  this fails if there's a
+    # conflict or something
+    if ( $post->{accttype} ) {
+        my ( $rv, $err ) = $cart->add_item(
+            DW::Shop::Item::Account->new( type => $post->{accttype}, %item_data )
+        );
+        return ( error => $err ) unless $rv;
+    }
+
+    return;
+}
+
+1;
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/shop.bml
--- a/htdocs/shop.bml	Tue Apr 14 07:34:07 2009 +0000
+++ b/htdocs/shop.bml	Tue Apr 14 08:07:41 2009 +0000
@@ -7,6 +7,7 @@
 #
 # Authors:
 #      Mark Smith <mark@dreamwidth.org>
+#      Janine Costanzo <janine@netrophic.com>
 #
 # Copyright (c) 2009 by Dreamwidth Studios, LLC.
 #
@@ -21,41 +22,30 @@ body<=
     use strict;
     use vars qw/ %GET %POST $title /;
 
+    # WE ARE NOT OPEN FOR BUSINESS
+    return BML::redirect( "$LJ::SITEROOT/" )
+        unless $LJ::IS_DEV_SERVER;
+
     # this page uses new style JS
-    LJ::need_res( 'stc/widget/shop.css' );
+    LJ::need_res( 'stc/shop.css' );
     LJ::set_active_resource_group( 'jquery' );
 
-    # the basic shop page is a collection of widgets!  thanks Janine :)
-    return DW::Widget::ShopCartStatusBar->render( %GET, minimal => 1 );
+    $title = BML::ml( '.title', { sitename => $LJ::SITENAMESHORT } );
+
+    my $ret;
+
+    $ret .= DW::Widget::ShopCartStatusBar->render( %GET );
+    $ret .= "<p>" . BML::ml( '.intro', { sitename => $LJ::SITENAMESHORT } ) . "</p>";
+
+    $ret .= DW::Widget::ShopItemGroupDisplay->render( group => 'paidaccounts' );
+
+    $ret .= "<div class='shopbox'>";
+    $ret .= "<p>" . BML::ml( '.sideblurb', { sitename => $LJ::SITENAMESHORT, aopts => "href='$LJ::HELPURL{paidaccountinfo}'" } ). "</p>";
+    $ret .= "</div>";
+
+    return $ret;
 }
 _code?>
-
-
-<?p Welcome to the Dreamwidth store!  If you are interested in supporting Dreamwidth Studios
-or are just looking for more features for your account, you have come to the right place. 
-I admit this page is pretty ugly and hope that someone will fix it. p?>
-
-<div id='left' class='shopbox'>
-    <?p If you are looking for a paid account, you pretty much have three options... p?>
-    <ul>
-        <li><a href='<?siteroot?>/shop/account?for=self'>...for yourself</a></li>
-        <li><a href='<?siteroot?>/shop/account?for=gift'>...for a gift</a></li>
-        <li><a href='<?siteroot?>/shop/account?for=new'>...a new account</a></li>
-    </ul>
-</div>
-
-<div id='right' class='shopbox'>
-    <?p This box has some information about Dreamwidth, a promotional blurb or
-    some other thing to tell you about us. p?>
-    <?p There is likely going to be a really compelling bunch of text here, but darn
-    if someone else has to write it. (And English strip it.) p?>
-    <?p Well, please enjoy Dreamwidth! p?>
-</div>
-
-
-
-
-
 <=body
-title=><?_ml .title _ml?>
+title=><?_code return $title; _code?>
 page?>
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/shop.bml.text
--- a/htdocs/shop.bml.text	Tue Apr 14 07:34:07 2009 +0000
+++ b/htdocs/shop.bml.text	Tue Apr 14 08:07:41 2009 +0000
@@ -1,4 +1,7 @@
 ;; -*- coding: utf-8 -*-
-.title=The Store
 
+.intro=Welcome to the [[sitename]] shop!  If you are interested in supporting [[sitename]] or are just looking for more features for your account, you have come to the right place.
 
+.sideblurb=You can learn about paid accounts <a [[aopts]]>here</a>.
+
+.title=[[sitename]] Shop
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/shop/account.bml
--- a/htdocs/shop/account.bml	Tue Apr 14 07:34:07 2009 +0000
+++ b/htdocs/shop/account.bml	Tue Apr 14 08:07:41 2009 +0000
@@ -7,6 +7,7 @@
 #
 # Authors:
 #      Mark Smith <mark@dreamwidth.org>
+#      Janine Costanzo <janine@netrophic.com>
 #
 # Copyright (c) 2009 by Dreamwidth Studios, LLC.
 #
@@ -22,7 +23,7 @@ body<=
     use vars qw/ %GET %POST $title /;
 
     # this page uses new style JS
-    LJ::need_res( 'stc/widget/shop.css' );
+    LJ::need_res( 'stc/shop.css' );
     LJ::set_active_resource_group( 'jquery' );
 
     # let's see what they're trying to do
@@ -30,120 +31,100 @@ body<=
     return BML::redirect( "$LJ::SITEROOT/shop" )
         unless $for && $for =~ /^(?:self|gift|new)$/;
 
+    $title = $ML{'.title'};
+
     # ensure they have a user if it's for self
     my $remote = LJ::get_remote();
-    return 'need a remote boss!'
-        if $for eq 'self' && ! $remote;
+    return $ML{'.error.invalidself'}
+        if $for eq 'self' && ( !$remote || !$remote->is_personal );
 
-    # setup the output
     my $ret = DW::Widget::ShopCartStatusBar->render( %GET );
-    $ret .= qq{
-<div class="leftybox">Yep, this is the page where you buy a paid account.  We could put some really awesome
-text in this box to tell you what about paid accounts is awesome.</div>
-    };
 
-    # show account status box
-    $ret .= DW::Widget::PaidAccountStatus->render;
+    $ret .= "<p><a href='$LJ::SITEROOT/shop'>&lt;&lt; " . BML::ml( '.backlink', { sitename => $LJ::SITENAMESHORT } ) . "</a></p>";
 
-    # if they posted...
-    my $try_post = sub {
-        return 'Faiiiiiil'
+    if ( $for eq 'self' ) {
+        $ret .= "<div class='leftybox'>" . BML::ml( '.intro.self', { user => $remote->ljuser_display } ) . "</div>";
+        $ret .= DW::Widget::PaidAccountStatus->render;
+    } elsif ( $for eq 'gift' ) {
+        $ret .= "<p>$ML{'.intro.gift'}</p>";
+    } else { # $for eq 'new'
+        $ret .= "<p>$ML{'.intro.new'}</p>";
+    }
+
+    if ( LJ::did_post() ) {
+        return "<?h1 $ML{'Error'} h1?><?p $ML{'error.invalidform'} p?>"
             unless LJ::check_form_auth();
 
-        my $at = $POST{accttype};
-        return 'You must select an account type'
-            unless $at && exists $LJ::SHOP{$at};
-
-        # now try to add this item to their list
-        my $cart = DW::Shop->get->cart
-            or return 'Failed to get a shopping cart for you, please try again later.';
-
-        my %who_for;
-        if ( $for eq 'self' ) {
-            my $remote = LJ::get_remote()
-                or return '<?needlogin?>';
-            $who_for{target_userid} = $remote->id;
-
-        } elsif ( $for eq 'gift' ) {
-            # FIXME: try to validate the email address
-            $who_for{target_email} = $POST{str};
-
-        } elsif ( $for eq 'new' ) {
-            my $un = LJ::canonical_username( $POST{str} );
-            return 'Invalid username'
-                unless $un;
-            return 'Username already in use'
-                if LJ::load_user( $un );
-
-            # FIXME: also, we should put a hold on this username to prevent people from
-            # doubling up on a purchase
-            $who_for{target_username} = $un;
-
+        my $error;
+        my $post_fields = LJ::Widget::ShopItemOptions->post_fields( \%POST );
+        if ( keys %$post_fields ) { # make sure the user selected an account type
+            # need to do this because all of these form fields are in the BML page instead of in the widget
+            LJ::Widget->use_specific_form_fields( post => \%POST, widget => "ShopItemOptions", fields => [ qw( for username email deliverydate_mm deliverydate_dd deliverydate_yyyy anonymous ) ] );
+            my %from_post = LJ::Widget->handle_post( \%POST, ( 'ShopItemOptions' ) );
+            $error = $from_post{error} if $from_post{error};
+        } else {
+            $error = $ML{'.error.noselection'};
         }
 
-        # build a new item and try to toss it in the cart.  this fails if there's a
-        # conflict or something
-        my ( $rv, $err ) = $cart->add_item(
-            DW::Shop::Item::Account->new( type => $at, %who_for )
-        );
-        return $err unless $rv;
-
-        # to make this a gift for another account, simply change what target_userid
-        # is set to
-
-        # to make this something to be emailed to someone, set target_email
-
-        # to make this a purchase of a new account, set target_username
-
-        # since we updated their list, return them to this page
-        return BML::redirect( "$LJ::SITEROOT/shop/account?for=$for" );
-    };
-    if ( LJ::did_post() ) {
-        my $errs = $try_post->();
-        if ( $errs ) {
-            $ret .= qq{<div class="shop-error">$errs</div>};
+        if ( $error ) {
+            $ret .= qq{<div class="shop-error">$error</div>};
+        } else {
+            return BML::redirect( "$LJ::SITEROOT/shop/cart" );
         }
     }
 
-    # all done
+    $ret .= "<div style='clear: both;'></div>";
+    $ret .= "<form method='post'>";
+    $ret .= LJ::form_auth();
+    $ret .= "<table class='shop-table'><tr><td>";
+    $ret .= LJ::Widget::ShopItemOptions->render( option_name => 'accttype', item => 'prem' );
+    $ret .= "</td><td>";
+    $ret .= LJ::Widget::ShopItemOptions->render( option_name => 'accttype', item => 'paid' );
+    $ret .= "</tr>";
+
+    if ( DW::Pay::num_permanent_accounts_available() > 0 ) {
+        $ret .= "<tr><td colspan='2'>";
+        $ret .= LJ::Widget::ShopItemOptions->render( option_name => 'accttype', item => 'seed' );
+        $ret .= "</td></tr>";
+    }
+
+    $ret .= "</table>";
+
+    if ( $for =~ /^(?:gift|new)$/ ) {
+        $ret .= "<table class='shop-table-gift'>";
+
+        if ( $for eq 'gift' ) {
+            $ret .= "<tr><td>$ML{'.giftfor.username'}</td><td>" . LJ::html_text( { name => 'username' } ) . "</td></tr>";
+        } else { # $for eq 'new'
+            $ret .= "<tr><td>$ML{'.giftfor.email'}</td><td>" . LJ::html_text( { name => 'email' } ) . "</td></tr>";
+        }
+
+        $ret .= "<tr><td>$ML{'.giftfor.deliverydate'}</td>";
+        $ret .= "<td>" . LJ::html_datetime( {
+            name => 'deliverydate',
+            default => DateTime->today->date,
+            notime => 1,
+        } ) . "</td></tr>";
+        $ret .= "<tr><td>$ML{'.giftfor.anonymous'}</td>";
+        $ret .= "<td>" . LJ::html_check( {
+            name => 'anonymous',
+            value => 1,
+            selected => $remote ? 0 : 1,
+            disabled => $remote ? 0 : 1,
+        } ) . "</td></tr>";
+
+        $ret .= "</table>";
+    }
+
+    $ret .= LJ::html_hidden( for => $GET{for} );
+    $ret .= "<p>" . LJ::html_submit( $ML{'.btn.addtocart'} ) . "</p>";
+    $ret .= "</form>";
+
+    $ret .= "<p><a href='$LJ::SITEROOT/shop'>&lt;&lt; " . BML::ml( '.backlink', { sitename => $LJ::SITENAMESHORT } ) . "</a></p>";
+
     return $ret;
 }
 _code?>
-
-<div style='clear: both;'></div>
-<form method='post'>
-<?_code return LJ::form_auth(); _code?>
-<table class='shop-table'>
-<tr><td>
-
-<strong>Premium Paid Account</strong><br />
-<input type='radio' name='accttype' id='prem6' value='prem6'><label for='prem6'>6 months for $20 USD</label></input><br />
-<input type='radio' name='accttype' id='prem12' value='prem12'><label for='prem12'>1 year for $40 USD</label></input>
-
-</td><td>
-
-<strong>Paid Account</strong><br />
-<input type='radio' name='accttype' id='paid1' value='paid1'><label for='paid1'>1 month for $3 USD</label></input><br />
-<input type='radio' name='accttype' id='paid2' value='paid2'><label for='paid2'>2 months for $5 USD</label></input><br />
-<input type='radio' name='accttype' id='paid6' value='paid6'><label for='paid6'>6 months for $13 USD</label></input><br />
-<input type='radio' name='accttype' id='paid12' value='paid12'><label for='paid12'>1 year for $25 USD</label></input>
-
-</td><td>
-
-<strong>Seed Account</strong><br />
-<input type='radio' name='accttype' id='seed' value='seed'><label for='seed'>Forever for $200 USD</label></input>
-
-</td></tr>
-</table>
-
-username or email: <input type='text' name='str' /><br />
-<input type='submit' /><br />
-
-<?p Dear Janine, please add the rest of the page here. p?>
-
-
-</form>
-
 <=body
-title=>Buy Paid Time
+title=><?_code return $title; _code?>
 page?>
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/shop/account.bml.text
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/shop/account.bml.text	Tue Apr 14 08:07:41 2009 +0000
@@ -0,0 +1,25 @@
+;; -*- coding: utf-8 -*-
+
+.backlink=Back to [[sitename]] Shop
+
+.btn.addtocart=Add to Order
+
+.error.invalidself=You must be logged in as a personal account in order to purchase paid time for yourself.
+
+.error.noselection=You must select an item to add to your cart.
+
+.giftfor.anonymous=Anonymous gift?
+
+.giftfor.deliverydate=Delivery date:
+
+.giftfor.email=Email address to receive the account creation code for this account:
+
+.giftfor.username=Username to receive this account:
+
+.intro.gift=Please choose the type of paid account that you'd like to purchase for an existing account.
+
+.intro.new=Please choose the type of paid account that you'd like to purchase for a new account.
+
+.intro.self=Please choose the type of paid account that you'd like to purchase for your account [[user]].
+
+.title=Buy a Paid Account
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/shop/cart.bml
--- a/htdocs/shop/cart.bml	Tue Apr 14 07:34:07 2009 +0000
+++ b/htdocs/shop/cart.bml	Tue Apr 14 08:07:41 2009 +0000
@@ -6,6 +6,7 @@
 #
 # Authors:
 #      Mark Smith <mark@dreamwidth.org>
+#      Janine Costanzo <janine@netrophic.com>
 #
 # Copyright (c) 2009 by Dreamwidth Studios, LLC.
 #
@@ -21,44 +22,28 @@ body<=
     use vars qw/ %GET %POST $title /;
 
     # this page uses new style JS
-    LJ::need_res( 'stc/widget/shop.css' );
+    LJ::need_res( 'stc/shop.css' );
     LJ::set_active_resource_group( 'jquery' );
 
-    # build a cart
-    my $cart = DW::Shop->get->cart
-        or return 'Failed to get a shopping cart for you, please try again later.';
+    $title = $ML{'.title'};
 
-    # if they want us to remove...
-    my $cartid = $GET{cartid}+0;
-    my $itemid = $GET{itemid}+0;
-    my $action = $GET{action};
+    my $ret;
 
-    # remove the item then render the current cart
-    if ( $action eq 'remove' ) {
-        return 'Invalid cartid'
-            if $cart->id != $cartid;
-        return 'Failed to remove item'
-            unless $cart->remove_item( $itemid );
+    if ( LJ::did_post() ) {
+        my %from_post = LJ::Widget->handle_post( \%POST, ( 'ShopCart' ) );
+        $ret .= "<div class='shop-error'>$from_post{error}</div>"
+            if $from_post{error};
     }
 
-    # setup the output
-    my $ret = DW::Widget::ShopCartStatusBar->render( %GET );
+    $ret .= "<p><a href='$LJ::SITEROOT/shop'>&lt;&lt; $ML{'.backlink'}</a></p>";
 
-    # now render the contents of the cart
-    $ret .= '<ul>';
-    foreach my $item ( @{$cart->items} ) {
-        # FIXME: should require a POST to remove items
-        $ret .= '<li>[' . $item->id . ", <a href='$LJ::SITEROOT/shop/cart?cartid=" . $cart->id . "&itemid=";
-        $ret .= $item->id . "&action=remove'>remove</a>] ";
-        $ret .= $item->permanent ? '(permanent)' : ( '(' . $item->months . ' months)' );
-        $ret .= ' ' . $item->class . ' account for ' . $item->t_html;
-    }
-    $ret .= '</ul>';
+    $ret .= LJ::Widget::ShopCart->render;
 
-    # all done
+    $ret .= "<p><a href='$LJ::SITEROOT/shop'>&lt;&lt; $ML{'.backlink'}</a></p>";
+
     return $ret;
 }
 _code?>
 <=body
-title=>Your Shopping Cart
+title=><?_code return $title; _code?>
 page?>
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/shop/cart.bml.text
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/shop/cart.bml.text	Tue Apr 14 08:07:41 2009 +0000
@@ -0,0 +1,5 @@
+;; -*- coding: utf-8 -*-
+
+.backlink=Continue Shopping
+
+.title=Your Shopping Cart
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/shop/index.bml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/shop/index.bml	Tue Apr 14 08:07:41 2009 +0000
@@ -0,0 +1,32 @@
+<?_c
+#
+# /shop/index.bml
+#
+# This is just a stub page that redirects to /shop.bml.
+#
+# Authors:
+#      Janine Costanzo <janine@netrophic.com>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+_c?><?page
+body<=
+<?_code
+{
+    use strict;
+    use vars qw/ %GET %POST $title $windowtitle $headextra @errors @warnings /;
+
+    return BML::redirect( "$LJ::SITEROOT/shop" );
+}
+_code?>
+<=body
+title=><?_code return $title; _code?>
+windowtitle=><?_code return $windowtitle; _code?>
+head<=
+<?_code return $headextra; _code?>
+<=head
+page?>
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/stc/shop.css
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/stc/shop.css	Tue Apr 14 08:07:41 2009 +0000
@@ -0,0 +1,113 @@
+/*
+    stc/shop.css
+   
+    CSS classes for rendering the various shop widgets and components.
+   
+    Authors:
+         Mark Smith <mark@dreamwidth.org>
+         Janine Costanzo <janine@netrophic.com>
+   
+    Copyright (c) 2009 by Dreamwidth Studios, LLC.
+   
+    This program is free software; you may redistribute it and/or modify it under
+    the same terms as Perl itself.  For a copy of the license, please reference
+    'perldoc perlartistic' or 'perldoc perlgpl'.
+*/
+
+
+.shopbox, .appwidget-shopitemgroupdisplay {
+    border: 1px solid #c1272d;
+    margin: 10px;
+    padding: 5px;
+    min-width: 30em;
+    max-width: 35em;
+    min-height: 20em;
+    float: left;
+}
+
+.leftybox {
+    max-width: 35em;
+    float: left;
+    margin: 10px;
+}
+
+.shop-account-status {
+    border: solid 1px #c1272d;
+    background-color: #ffff00;
+    float: right;
+    width: 30em;
+    padding: 5px;
+    margin: 10px;
+}
+
+.shop-table {
+    margin: 1em auto;
+    min-width: 50em;
+}
+
+.shop-table td {
+    vertical-align: top;
+    padding-left: 1em;
+}
+
+.shop-table-gift {
+    margin: 1em;
+}
+
+.shop-table-gift td {
+    vertical-align: top;
+    padding: 0.2em;
+}
+
+.shop-error {
+    border: 1px solid #c1272d;
+    clear: both;
+    padding: 5px;
+    margin: 10px;
+}
+
+.shop-cart-status {
+    border: 1px solid #c1272d;
+    background-color: #c5c5c5;
+    padding: 0.5em;
+}
+
+.shop-cart-status ul {
+    list-style: none;
+    margin: 0;
+    padding-bottom: 1.5em;
+}
+
+.shop-cart-status ul li {
+    float: left;
+    margin-right: 1em;
+}
+
+.shop-cart {
+    margin: 1em;
+}
+
+.shop-cart th {
+    background-color: #c5c5c5;
+}
+
+.shop-cart td, .shop-cart th {
+    border: 1px solid #c1272d;
+    padding: 0.5em;
+    text-align: center;
+}
+
+.shop-cart td.total {
+    font-weight: bold;
+    text-align: right;
+    background-color: #e0e0e0;
+}
+
+.shop-cart-btn {
+    margin-left: 1em;
+}
+
+.shop-item-highlight {
+    color: #c1272d;
+    font-weight: bold;
+}
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/stc/widgets/shop.css
--- a/htdocs/stc/widgets/shop.css	Tue Apr 14 07:34:07 2009 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-/*
-    stc/widgets/shop.css
-   
-    CSS classes for rendering the various shop widgets and components.
-   
-    Authors:
-         Mark Smith <mark@dreamwidth.org>
-   
-    Copyright (c) 2009 by Dreamwidth Studios, LLC.
-   
-    This program is free software; you may redistribute it and/or modify it under
-    the same terms as Perl itself.  For a copy of the license, please reference
-    'perldoc perlartistic' or 'perldoc perlgpl'.
-*/
-
-
-.shop-browse-separator {
-    padding-top: 10px;    
-}
-
-.shopbox {
-    border: solid 1px red;
-    margin: 10px;
-    padding: 5px;
-    max-width: 35em;
-    min-height: 20em;
-}
-
-.leftybox {
-    max-width: 35em;
-    float: left;
-    margin: 10px;
-}
-
-#left {
-    float: left;
-}
-
-#right {
-    float: right;
-}
-
-.shop-account-status {
-    border: solid 1px red;
-    background-color: yellow;
-    float: right;
-    width: 30em;
-    padding: 5px;
-    margin: 10px;
-}
-
-.shop-table {
-    margin: 1em auto;
-    min-width: 50em;
-}
-
-.shop-table td {
-    vertical-align: top;
-}
-
-.shop-error {
-    border: solid 1px red;
-    clear: both;
-    padding: 5px;
-    margin: 10px;
-}
--------------------------------------------------------------------------------

Post a comment in response:

This account has disabled anonymous posting.
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

If you are unable to use this captcha for any reason, please contact us by email at support@dreamwidth.org