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] changelog2012-07-21 12:45 am

[dw-free] Add extra icon purchasing

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

Add extra icon purchasing

This lets users purchase permanent extra icons for their accounts. They cost
$1 USD (or 10 points) each and never expire. This also includes a console
tool for admins to maintain them.

This does not yet support transferring between accounts. We will do that
soon.

Patch by [staff profile] mark.

Files modified:
  • bin/upgrading/en.dat
  • bin/upgrading/proplists.dat
  • cgi-bin/DW/Console/Command/BonusIcons.pm
  • cgi-bin/DW/Controller/Shop.pm
  • cgi-bin/DW/Shop.pm
  • cgi-bin/DW/Shop/Item/Icons.pm
  • cgi-bin/LJ/Global/Defaults.pm
  • cgi-bin/LJ/User.pm
  • cvs/multicvs.conf
  • views/shop/icons.tt
  • views/shop/icons.tt.text
  • views/shop/index.tt
  • views/shop/index.tt.text
--------------------------------------------------------------------------------
diff -r db1baaa9ddac -r 1ead40895ac9 bin/upgrading/en.dat
--- a/bin/upgrading/en.dat	Thu Jul 19 21:58:03 2012 -0700
+++ b/bin/upgrading/en.dat	Fri Jul 20 17:49:33 2012 -0700
@@ -3250,7 +3250,7 @@
 
     [[gift]]
 
-
+[[extra]]
 Regards,
 The [[sitename]] Team
 .
@@ -3264,7 +3264,7 @@
 
     [[gift]]
 
-
+[[extra]]
 Regards,
 The [[sitename]] Team
 .
@@ -3572,6 +3572,29 @@
 
 shop.item.account.randomuser=Random active free user
 
+shop.item.icons.canbeadded.banned=You are restricted from making purchases for this journal.
+
+shop.item.icons.canbeadded.invalidjournaltype=You can only buy icons for a personal journal.
+
+shop.item.icons.canbeadded.itemerror=Could not add item to cart.
+
+shop.item.icons.canbeadded.notauser=You can only buy icons for an active account.
+
+shop.item.icons.canbeadded.notpaid=That account is ineligible for extra icons. These can only be purchased for a paid account.
+
+shop.item.icons.canbeadded.outofrange=You can only buy up to [[count]] icons for this journal.
+
+shop.item.icons.name=[[num]] [[sitename]] icons
+
+shop.item.icons.overlimit<<
+NOTE: This order has pushed your account over the [[sitename]] limit
+of [[max]] icons. If you would like to transfer the surplus icons to
+another account, or convert them into [[sitename]] Points, please
+contact us for assistance.
+
+
+.
+
 shop.item.points.canbeadded.banned=You are restricted from making purchases for this journal.
 
 shop.item.points.canbeadded.insufficient=You do not have enough points to do that.
diff -r db1baaa9ddac -r 1ead40895ac9 bin/upgrading/proplists.dat
--- a/bin/upgrading/proplists.dat	Thu Jul 19 21:58:03 2012 -0700
+++ b/bin/upgrading/proplists.dat	Fri Jul 20 17:49:33 2012 -0700
@@ -46,6 +46,14 @@
   multihomed: 0
   prettyname: Beta Features List
 
+userproplist.bonus_icons:
+  cldversion: 0
+  datatype: num
+  des: Count of permanent extra icon slots
+  indexed: 0
+  multihomed: 0
+  prettyname: How many bonus icon slots the user has purchased
+
 userproplist.browselang:
   cldversion: 4
   datatype: char
diff -r db1baaa9ddac -r 1ead40895ac9 cgi-bin/DW/Console/Command/BonusIcons.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Console/Command/BonusIcons.pm	Fri Jul 20 17:49:33 2012 -0700
@@ -0,0 +1,81 @@
+#!/usr/bin/perl
+#
+# DW::Console::Command::BonusIcons
+#
+# Console commands for managing bonus icons.
+#
+# Authors:
+#      Mark Smith <mark@dreamwidth.org>
+#
+# Copyright (c) 2012 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::Console::Command::BonusIcons;
+use strict;
+
+use base qw/ LJ::Console::Command /;
+use Carp qw/ croak /;
+use List::Util qw/ max /;
+
+sub cmd { 'bonus_icons' }
+sub desc { 'Manage bonus icons for an account.' }
+sub args_desc {
+    [
+        'command' => 'Subcommand: add, remove.',
+        'username' => 'Username to act on.',
+        'count' => 'How many icons to add or remove.',
+    ]
+}
+sub usage { '<username> [<subcommand> <count>]' }
+sub can_execute { 1 }
+
+sub execute {
+    my ( $self, $user, $cmd, $count ) = @_;
+
+    my $remote = LJ::get_remote();
+    return $self->error( 'You must be logged in!' )
+        unless $remote;
+    return $self->error( 'I\'m afraid I can\'t let you do that.' )
+        unless $remote->has_priv( 'payments' => 'bonus_icons' );
+
+    my $to_u = LJ::load_user( $user );
+    return $self->error( 'Invalid user.' )
+        unless $to_u;
+
+    unless ( defined $cmd ) {
+        # No subcommand to add or remove. Just print how many icons they have.
+        return $self->print( sprintf( '%s has %d bonus icons.',
+                $to_u->user, $to_u->prop( 'bonus_icons' ) ) );
+    }
+
+    return $self->error( 'Invalid subcommand.' )
+        if $cmd && $cmd !~ /^(?:add|remove)$/;
+
+    return $self->error( 'Count must be a positive integer.' )
+        unless $count =~ /^\d+$/;
+    $count += 0;
+
+    if ( $cmd eq 'add' ) {
+        my $new = max( $to_u->prop( 'bonus_icons' ) + $count, 0 );
+        $to_u->set_prop( bonus_icons => $new );
+        LJ::statushistory_add( $to_u, $remote, 'bonus_icons',
+                sprintf( 'Added %d icons, new total: %d.', $count, $new ) );
+        $self->print( sprintf( 'User now has %d icons.', $new ) );
+
+    } elsif ( $cmd eq 'remove' ) {
+        my $new = max( $to_u->prop( 'bonus_icons' ) - $count, 0 );
+        $to_u->set_prop( bonus_icons => $new );
+        LJ::statushistory_add( $to_u, $remote, 'bonus_icons',
+                sprintf( 'Removed %d icons, new total: %d.', $count, $new ) );
+        $self->print( sprintf( 'User now has %d icons.', $new ) );
+
+    }
+
+    return 1;
+}
+
+1;
diff -r db1baaa9ddac -r 1ead40895ac9 cgi-bin/DW/Controller/Shop.pm
--- a/cgi-bin/DW/Controller/Shop.pm	Thu Jul 19 21:58:03 2012 -0700
+++ b/cgi-bin/DW/Controller/Shop.pm	Fri Jul 20 17:49:33 2012 -0700
@@ -27,6 +27,7 @@
 # routing directions
 DW::Routing->register_string( '/shop', \&shop_index_handler, app => 1 );
 DW::Routing->register_string( '/shop/points', \&shop_points_handler, app => 1 );
+DW::Routing->register_string( '/shop/icons', \&shop_icons_handler, app => 1 );
 DW::Routing->register_string( '/shop/transferpoints', \&shop_transfer_points_handler, app => 1 );
 
 # our basic shop controller, this does setup that is unique to all shop
@@ -238,5 +239,62 @@
     return DW::Template->render_template( 'shop/points.tt', $rv );
 }
 
+# handles the shop buy icons page
+sub shop_icons_handler {
+    my ( $ok, $rv ) = _shop_controller();
+    return $rv unless $ok;
+
+    my $remote = $rv->{remote};
+    my %errs;
+    $rv->{errs} = \%errs;
+
+    my $r = DW::Request->get;
+    if ( $r->did_post ) {
+        my $args = $r->post_args;
+        die "invalid auth\n" unless LJ::check_form_auth( $args->{lj_form_auth} );
+
+        my $u = LJ::load_user( $args->{foruser} );
+        my $icons = int( $args->{icons} + 0 );
+        my $item;  # provisionally create the item to access object methods
+
+        if ( !$u ) {
+            $errs{foruser} = LJ::Lang::ml( 'shop.item.icons.canbeadded.notauser' );
+
+        } elsif ( $item = DW::Shop::Item::Icons->new( target_userid => $u->id, from_userid => $remote->id, icons => $icons ) ) {
+            # error check the user
+            if ( $item->can_be_added_user( errref => \$errs{foruser} ) ) {
+                $rv->{foru} = $u;
+                delete $errs{foruser};  # undefined
+            }
+
+            # error check the icons
+            if ( $item->can_be_added_icons( errref => \$errs{icons} ) ) {
+                $rv->{icons} = $icons;
+                delete $errs{icons};  # undefined
+            }
+
+        } else {
+            $errs{foruser} = LJ::Lang::ml( 'shop.item.icons.canbeadded.itemerror' );
+        }
+
+        # looks good, add it!
+        unless ( keys %errs ) {
+            $rv->{cart}->add_item( $item );
+            return $r->redirect( "$LJ::SITEROOT/shop" );
+        }
+
+    } else {
+        my $for = $r->get_args->{for};
+
+        if ( ! $for || $for eq 'self' ) {
+            $rv->{foru} = $remote;
+        } else {
+            $rv->{foru} = LJ::load_user( $for );
+        }
+    }
+
+    return DW::Template->render_template( 'shop/icons.tt', $rv );
+}
+
 
 1;
diff -r db1baaa9ddac -r 1ead40895ac9 cgi-bin/DW/Shop.pm
--- a/cgi-bin/DW/Shop.pm	Thu Jul 19 21:58:03 2012 -0700
+++ b/cgi-bin/DW/Shop.pm	Fri Jul 20 17:49:33 2012 -0700
@@ -25,6 +25,7 @@
 use DW::Shop::Item::Account;
 use DW::Shop::Item::Points;
 use DW::Shop::Item::Rename;
+use DW::Shop::Item::Icons;
 
 # constants across the site
 our $MIN_ORDER_COST = 3.00; # cost in USD minimum.  this only comes into affect if
@@ -189,5 +190,4 @@
         or confess 'tried to get shop without calling DW::Shop->initialize()';
 }
 
-
 1;
diff -r db1baaa9ddac -r 1ead40895ac9 cgi-bin/DW/Shop/Item/Icons.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Shop/Item/Icons.pm	Fri Jul 20 17:49:33 2012 -0700
@@ -0,0 +1,230 @@
+#!/usr/bin/perl
+#
+# DW::Shop::Item::Icons
+#
+# Represents Dreamwidth Icons that someone is buying.
+#
+# Authors:
+#      Mark Smith <mark@dreamwidth.org>
+#
+# Copyright (c) 2012 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::Shop::Item::Icons;
+
+use base 'DW::Shop::Item';
+
+use strict;
+use DW::InviteCodes;
+use DW::Pay;
+
+=head1 NAME
+
+DW::Shop::Item::Icons - Represents extra icons that someone is purchasing. See
+the documentation for DW::Shop::Item for usage examples and description of methods
+inherited from that base class.
+
+=head1 API
+
+=head2 C<< $class->new( [ %args ] ) >>
+
+Instantiates a block of icons to be purchased.
+
+Arguments:
+=item ( see DW::Shop::Item ),
+=item icons => number of icons to buy,
+
+=cut
+
+# override
+sub new {
+    my ( $class, %args ) = @_;
+
+    my $self = $class->SUPER::new( %args, type => 'icons' );
+    return unless $self;
+
+    # Set up our initial cost structure
+    $self->{cost_cash} = $self->{icons};
+    $self->{cost_points} = 0;
+
+    # for now, we can only apply to a user.  in the future this is an obvious way
+    # to do gift certificates by allowing an email address here...
+    die "Can only give icons to an account.\n"
+        unless $self->t_userid;
+
+    return $self;
+}
+
+
+# override
+sub _apply {
+    my $self = $_[0];
+
+    return $self->_apply_userid if $self->t_userid;
+
+    # something weird, just kill this item!
+    $self->{applied} = 1;
+    return 1;
+}
+
+
+# internal application sub, do not call
+sub _apply_userid {
+    my $self = $_[0];
+    return 1 if $self->applied;
+
+    # will need this later
+    my $fu = LJ::load_userid( $self->from_userid );
+    unless ( $fu ) {
+        warn "Failed to apply: invalid from_userid!\n";
+        return 0;
+    }
+
+    # need this user
+    my $u = LJ::load_userid( $self->t_userid )
+        or return 0;
+
+    # validate that they can get this number of icons
+    my $cur = $u->prop( 'bonus_icons' ) // 0;
+    $u->set_prop( bonus_icons => $cur + $self->icons );
+    LJ::statushistory_add( $u, $fu, 'bonus_icons',
+            sprintf( '%d icons added; item #%d', $self->icons, $self->id ) );
+
+    # we're applied now, regardless of what happens with the email
+    $self->{applied} = 1;
+
+    # see if this has put the user over their limit
+    my $overlimit = '';
+    my $real_total = $self->icons + $u->get_cap( 'userpics' ) + $cur;
+    if ( $real_total > $LJ::USERPIC_MAXIMUM ) {
+        $overlimit = LJ::Lang::ml( 'shop.item.icons.overlimit',
+                { sitename => $LJ::SITENAMESHORT, max => $LJ::USERPIC_MAXIMUM,
+                  overage => $real_total - $LJ::USERPIC_MAXIMUM } );
+    }
+
+    # now we have to mail this notification
+    my $word = $fu->equals( $u ) ? 'self' : 'other';
+    my $body = LJ::Lang::ml( "shop.email.gift.$word.body",
+        {
+            touser => $u->display_name,
+            fromuser => $fu->display_name,
+            sitename => $LJ::SITENAME,
+            gift => sprintf( '%d %s Extra Icons', $self->icons, $LJ::SITENAMESHORT ),
+            extra => $overlimit,
+        }
+    );
+    my $subj = LJ::Lang::ml( "shop.email.gift.$word.subject", { sitename => $LJ::SITENAME } );
+
+    # send the email to the user
+    LJ::send_mail( {
+        to       => $u->email_raw,
+        from     => $LJ::ACCOUNTS_EMAIL,
+        fromname => $LJ::SITENAME,
+        subject  => $subj,
+        body     => $body
+    } );
+
+    # tell the caller we're happy
+    return 1;
+}
+
+
+# override
+sub unapply {
+    my $self = $_[0];
+    return unless $self->applied;
+
+    # unapplying is not coded yet, as we don't have good automatic support for orders being
+    # reverted and refunded.
+    $self->{applied} = 0;
+    die "Unable to unapply right now.\n";
+
+    return 1;
+}
+
+
+# override
+sub can_be_added {
+    my ( $self, %opts ) = @_;
+
+    return 0 unless $self->can_be_added_user( %opts );
+    return 0 unless $self->can_be_added_icons( %opts );
+
+    return 1;
+}
+
+
+sub can_be_added_user {
+    my ( $self, %opts ) = @_;
+    my $errref = $opts{errref};
+
+    # if not a valid account, error
+    my $target_u = LJ::load_userid( $self->t_userid );
+    if ( ! LJ::isu( $target_u ) ) {
+        $$errref = LJ::Lang::ml( 'shop.item.icons.canbeadded.notauser' );
+        return 0;
+    }
+
+    # the receiving user must be a person for now
+    unless ( $target_u->is_personal && $target_u->is_visible ) {
+        $$errref = LJ::Lang::ml( 'shop.item.icons.canbeadded.invalidjournaltype' );
+        return 0;
+    }
+
+    # and they must be paid
+    unless ( $target_u->can_buy_icons ) {
+        $$errref = LJ::Lang::ml( 'shop.item.icons.canbeadded.notpaid' );
+        return 0;
+    }
+
+    # make sure no sysban is in effect here
+    my $fromu = LJ::load_userid( $self->from_userid );
+    if ( $fromu && $target_u->has_banned( $fromu ) ) {
+        $$errref = LJ::Lang::ml( 'shop.item.icons.canbeadded.banned' );
+        return 0;
+    }
+
+    return 1;
+}
+
+
+sub can_be_added_icons {
+    my ( $self, %opts ) = @_;
+    my $errref = $opts{errref};
+
+    # sanity check that the icons to add are within range
+    my $target_u = LJ::load_userid( $self->t_userid );
+    my $pics_left = $LJ::USERPIC_MAXIMUM - $target_u->userpic_quota;
+    unless ( $self->icons > 0 && $self->icons <= $pics_left ) {
+        $$errref = LJ::Lang::ml( 'shop.item.icons.canbeadded.outofrange',
+                { count => $pics_left });
+        return 0;
+    }
+
+    return 1;
+}
+
+
+# override
+sub name_text {
+    my $self = $_[0];
+
+    return LJ::Lang::ml( 'shop.item.icons.name',
+            { num => $self->icons, sitename => $LJ::SITENAMESHORT } );
+}
+
+
+=head2 C<< $self->icons >>
+
+Return how many icons this item is worth.
+
+=cut
+
+sub icons { $_[0]->{icons} }
+
+
+1;
diff -r db1baaa9ddac -r 1ead40895ac9 cgi-bin/LJ/Global/Defaults.pm
--- a/cgi-bin/LJ/Global/Defaults.pm	Thu Jul 19 21:58:03 2012 -0700
+++ b/cgi-bin/LJ/Global/Defaults.pm	Fri Jul 20 17:49:33 2012 -0700
@@ -371,6 +371,9 @@
 
     # Secrets
     %SECRETS = () unless defined %SECRETS;
+
+    # Userpic maximum. No user can have more than this.
+    $USERPIC_MAXIMUM ||= 500;
 }
 
 
diff -r db1baaa9ddac -r 1ead40895ac9 cgi-bin/LJ/User.pm
--- a/cgi-bin/LJ/User.pm	Thu Jul 19 21:58:03 2012 -0700
+++ b/cgi-bin/LJ/User.pm	Fri Jul 20 17:49:33 2012 -0700
@@ -1,3 +1,6 @@
+#
+# NOTE: This module now requires Perl 5.10 or greater.
+#
 # This code was forked from the LiveJournal project owned and operated
 # by Live Journal, Inc. The code has been modified and expanded by
 # Dreamwidth Studios, LLC. These files were originally licensed under
@@ -32,8 +35,8 @@
 package LJ::User;
 use Carp;
 use Storable;
+use List::Util qw/ min /;
 use lib "$LJ::HOME/cgi-bin";
-use List::Util ();
 use LJ::Global::Constants;
 use LJ::MemCache;
 use LJ::Session;
@@ -1946,6 +1949,10 @@
     return $_[0]->get_cap( 'beta_payments' ) ? 1 : 0;
 }
 
+sub can_buy_icons {
+    return $_[0]->get_cap( 'bonus_icons' ) ? 1 : 0;
+}
+
 sub can_create_feeds {
     return $_[0]->get_cap( 'synd_create' ) ? 1 : 0;
 }
@@ -2324,7 +2331,7 @@
 }
 
 sub count_max_userpics {
-    return $_[0]->get_cap( 'userpics' );
+    return $_[0]->userpic_quota;
 }
 
 sub count_max_xpost_accounts {
@@ -3053,7 +3060,7 @@
 =cut
 
 sub shop_points {
-    return $_[0]->prop( 'shop_points' )+0;
+    return $_[0]->prop( 'shop_points' ) // 0;
 }
 
 
@@ -6412,7 +6419,6 @@
     # active / inactive lists
     my @active = ();
     my @inactive = ();
-    my $allow = LJ::get_cap($u, "userpics");
 
     # get a database handle for reading/writing
     my $dbh = LJ::get_db_writer();
@@ -6432,8 +6438,9 @@
     }
 
     # inactivate previously activated userpics
-    if (@active > $allow) {
-        my $to_ban = @active - $allow;
+    my $allowed = $u->userpic_quota;
+    if (scalar @active > $allowed) {
+        my $to_ban = scalar @active - $allowed;
 
         # find first jitemid greater than time 2 months ago using rlogtime index
         # ($LJ::EndOfTime - UnixTime)
@@ -6491,8 +6498,8 @@
     }
 
     # activate previously inactivated userpics
-    if (@inactive && @active < $allow) {
-        my $to_activate = $allow - @active;
+    if (scalar @inactive && scalar @active < $allowed) {
+        my $to_activate = $allowed - @active;
         $to_activate = @inactive if $to_activate > @inactive;
 
         # take the $to_activate newest (highest numbered) pictures
@@ -7217,17 +7224,17 @@
     return $_[0]->dversion >= 9;
 }
 
-
 =head3 C<< $u->userpic_quota >>
 
-Returns the number of userpics the user can upload
+Returns the number of userpics the user can upload (base account type cap + bonus slots purchased)
 
 =cut
 sub userpic_quota {
     my $u = shift or return undef;
-    my $quota = $u->get_cap('userpics');
-
-    return $quota;
+    my $ct = $u->get_cap( 'userpics' );
+    $ct += $u->prop('bonus_icons') // 0
+        if $u->is_paid; # paid accounts get bonus icons
+    return min( $ct, $LJ::USERPIC_MAXIMUM );
 }
 
 # Intentionally no POD here.
diff -r db1baaa9ddac -r 1ead40895ac9 cvs/multicvs.conf
--- a/cvs/multicvs.conf	Thu Jul 19 21:58:03 2012 -0700
+++ b/cvs/multicvs.conf	Fri Jul 20 17:49:33 2012 -0700
@@ -11,7 +11,7 @@
 CVSDIR=$LJHOME/cvs
 
 # DreamWidth repositories
-HG(dw-free)               = http://hg.dwscoalition.org/dw-free @stable
+#HG(dw-free)               = http://hg.dwscoalition.org/dw-free @stable
 
 # stock/unchanged repositories pulled from external sources
 SVN(gearman)              = http://code.livejournal.org/svn/gearman/trunk/
diff -r db1baaa9ddac -r 1ead40895ac9 views/shop/icons.tt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/views/shop/icons.tt	Fri Jul 20 17:49:33 2012 -0700
@@ -0,0 +1,38 @@
+[%- sections.title = '.title' | ml(sitename = site.nameshort) -%]
+
+[% cart_display %]
+
+<p>[% '.about' | ml(sitename = site.nameshort) %]</p>
+
+<form method='post'>
+[% dw.form_auth %]
+<table summary='' class='shop-table-gift'>
+[% IF foru %]
+    <tr><td>[% '.buying.for' | ml %]</td><td>[% foru.ljuser_display %]
+        <input type='hidden' name='foruser' value='[% foru.user %]' />
+        <input type='hidden' name='maxicons' value='[% maxicons %]' />
+    </td></tr>
+[% ELSE %]
+    <tr><td>[% '.buying.for' | ml %]</td><td><input type='text' name='foruser' maxlength='25' size='15' />
+        [% IF errs.foruser %]<br /><strong>[% errs.foruser %][% END %]
+    </td></tr>
+[% END %]
+<tr><td>[% '.buying.icons' | ml %]</td>
+    <td><input type='text' name='icons' id='icons' maxlength='3' size='10' value='[% icons %]' />
+    [% IF errs.icons %]<br /><strong>[% errs.icons %][% END %]
+</td></tr>
+<tr><td><span id='icons-cost'></span></td><td><input type='submit' value='[% '.addtocart' | ml %]' /></td></tr>
+</table>
+</form>
+
+<p id='icons-about'>[% '.about2' | ml %]</p>
+
+[%# FIXME: move this to shop.js or something %]
+<script type='text/javascript'>
+    jQuery( function($) {
+        setInterval(
+            function() {
+                $('#icons-cost').html( 'Cost: <strong>$' + ($('#icons').val() / 1).toFixed(2) + ' USD</strong>' );
+            }, 250 );
+    } );
+</script>
diff -r db1baaa9ddac -r 1ead40895ac9 views/shop/icons.tt.text
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/views/shop/icons.tt.text	Fri Jul 20 17:49:33 2012 -0700
@@ -0,0 +1,13 @@
+;; -*- coding: utf-8 -*-
+.about=This page allows you to buy extra icons for a [[sitename]] account.
+
+.about2=Extra icons purchased for an account never expire. Each extra icon you purchase will cost $1.00 USD.
+
+.addtocart=Add To Cart
+
+.buying.for=Buying for Account:
+
+.buying.icons=Extra Icons to Purchase:
+
+.title=[[sitename]] Extra Icons Shop
+
diff -r db1baaa9ddac -r 1ead40895ac9 views/shop/index.tt
--- a/views/shop/index.tt	Thu Jul 19 21:58:03 2012 -0700
+++ b/views/shop/index.tt	Fri Jul 20 17:49:33 2012 -0700
@@ -45,6 +45,19 @@
     </div>
 </div>
 
+<div class='shop-category'>
+    <div class='shop-category-title'>[% '.title.icons' | ml %]</div>
+    <div class='shop-category-items'>
+[% IF remote AND remote.is_personal %]
+        <span class='shop-category-item'><a href="[% site.root %]/shop/icons?for=self">[% '.for.self' | ml %]</a> ([% remote.ljuser_display %])</span>
+[% END %]
+[% IF remote AND remote.is_personal %]
+        <span class='shop-category-item'><a href="[% site.root %]/shop/icons?for=gift">[% '.for.different' | ml %]</a></span>
+[% ELSE %]
+        <span class='shop-category-item'><a href="[% site.root %]/shop/icons?for=gift">[% '.for.existing' | ml %]</a></span>
+[% END %]
+    </div>
+</div>
 [%#
 
 Here for future expansion ...
diff -r db1baaa9ddac -r 1ead40895ac9 views/shop/index.tt.text
--- a/views/shop/index.tt.text	Thu Jul 19 21:58:03 2012 -0700
+++ b/views/shop/index.tt.text	Fri Jul 20 17:49:33 2012 -0700
@@ -19,6 +19,8 @@
 
 .title=[[sitename]] Shop
 
+.title.icons=Buy extra icons for...
+
 .title.paidacc=Buy paid services for...
 
 .title.points=Buy [[site]] Points for...
--------------------------------------------------------------------------------