[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
janinedog.
Files modified:
http://bugs.dwscoalition.org/show_bug.cgi?id=116
More work on the shop interface.
Patch by
![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
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'><< " . 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'><< " . 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'><< $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'><< $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; -} --------------------------------------------------------------------------------