Second commit
authorMarius Gavrilescu <marius@ieval.ro>
Sat, 20 Feb 2016 19:11:10 +0000 (19:11 +0000)
committerMarius Gavrilescu <marius@ieval.ro>
Sat, 20 Feb 2016 19:11:10 +0000 (19:11 +0000)
15 files changed:
MANIFEST
Makefile.PL
db.sql
lib/App/Web/.#Oof.pm [deleted symlink]
lib/App/Web/Oof.pm
static/Eligible-Bold.ttf [new file with mode: 0644]
static/Eligible-Regular.ttf [new file with mode: 0644]
static/Gravity-Bold.otf [new file with mode: 0644]
static/style.css
tmpl/continue.html
tmpl/details.html [new file with mode: 0644]
tmpl/footer.html [new file with mode: 0644]
tmpl/form.html
tmpl/order.html
tmpl/pay.html [new file with mode: 0644]

index 3f7cc43a8380571d59af45fa82550f4f07427584..7151d876659f9df38fa703dad939a8aa29d698fb 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -5,10 +5,16 @@ lib/App/Web/Oof.pm
 Makefile.PL
 MANIFEST
 README
+static/Eligible-Bold.ttf
+static/Eligible-Regular.ttf
+static/Gravity-Bold.otf
 static/Gravity-UltraLight.otf
 static/pattern.png
 static/style.css
 t/App-Web-Oof.t
 tmpl/continue.html
+tmpl/details.html
+tmpl/footer.html
 tmpl/form.html
 tmpl/order.html
+tmpl/pay.html
index 278e38823507779651e4b1e709a122abae1ea95c..ffc13b0ffdba076b2c3db34124d006363abeb173 100644 (file)
@@ -13,9 +13,11 @@ WriteMakefile(
                qw/Plack::App::File       0
                   Plack::Builder         0
                   DBIx::Simple           0
+                  File::Slurp            0
                   HTML::TreeBuilder      0
                   HTML::Element::Library 0
-                  JSON::MaybeXS          0/,
+                  JSON::MaybeXS          0
+                  Try::Tiny              0/,
        },
        META_ADD         => {
                dynamic_config => 0,
diff --git a/db.sql b/db.sql
index 56edc0959d8cadfd863f976b076ee6c6f4da5254..d8691f4d5cafa479137ca5a2c27f2c3f5ef1ff26 100644 (file)
--- a/db.sql
+++ b/db.sql
@@ -3,7 +3,6 @@ CREATE TABLE IF NOT EXISTS products (
        title    TEXT   NOT NULL,
        subtitle TEXT   NOT NULL,
        summary  TEXT   NOT NULL,
-       pictures TEXT[]     NULL,
        price    INT    NOT NULL,
        stock    INT    NOT NULL,
        CONSTRAINT positive_stock CHECK (stock >= 0)
@@ -22,6 +21,8 @@ CREATE TABLE IF NOT EXISTS orders (
        total    INT NOT NULL,
        discount VARCHAR(20) REFERENCES discounts UNIQUE,
 
+       stripe_token TEXT,
+
        -- DELIVERY
        first_name   VARCHAR(20) NOT NULL,
        last_name    VARCHAR(20) NOT NULL,
diff --git a/lib/App/Web/.#Oof.pm b/lib/App/Web/.#Oof.pm
deleted file mode 120000 (symlink)
index 2792ff4..0000000
+++ /dev/null
@@ -1 +0,0 @@
-marius@mgvx.1727:1455362169
\ No newline at end of file
index b8fbaf5f637519f686489cb9d21e9875e285e29d..1f937fe182a6c1c4f0d13f569bd603562392df52 100644 (file)
@@ -9,11 +9,13 @@ use parent qw/Plack::Component/;
 our $VERSION = '0.000_001';
 
 use DBIx::Simple;
+use File::Slurp;
 use HTML::TreeBuilder;
 use HTML::Element::Library;
 use JSON::MaybeXS qw/encode_json decode_json/;
 use Plack::Builder;
 use Plack::Request;
+use Try::Tiny;
 
 sub HTML::Element::iter3 {
        my ($self, $data, $code) = @_;
@@ -33,8 +35,8 @@ sub HTML::Element::fclass { shift->look_down(class => qr/\b$_[0]\b/) }
 
 ##################################################
 
-my $db;
-my ($form, $continue, $order);
+my %db;
+my ($form, $continue, $order, $details, $pay);
 
 {
        sub parse_html {
@@ -47,25 +49,35 @@ my ($form, $continue, $order);
        $form     = parse_html 'form';
        $continue = parse_html 'continue';
        $order    = parse_html 'order';
+       $details  = parse_html 'details';
+       $pay      = parse_html 'pay';
 }
 
 sub stringify_money { sprintf "£%.2f", $_[0] / 100 }
 
+sub make_slug {
+       my $slug = $_[0];
+       $slug =~ y/ /-/;
+       $slug =~ y/a-zA-Z0-9-//cd;
+       $slug
+}
+
 sub form_table_row {
        my ($data, $tr) = @_;
        $tr->fclass($_)->replace_content($data->{$_}) for qw/title subtitle stock/;
        $tr->fclass('price')->replace_content(stringify_money $data->{price});
        $tr->fclass('title')->attr('data-product', $data->{product});
-       $tr->fclass('title')->attr('data-summary', $data->{summary});
+       $tr->fclass('title')->attr('href', '/details/'.$data->{product}.'/'.make_slug $data->{title});
+#      $tr->fclass('title')->attr('data-summary', $data->{summary});
        $tr->look_down(_tag => 'input')->attr(max => $data->{stock});
        $tr->look_down(_tag => 'input')->attr(name => 'quant'.$data->{product});
 }
 
 sub form_app {
        my ($env) = @_;
-       $db //= DBIx::Simple->connect($ENV{OOF_DSN} // 'dbi:Pg:');
+       $db{$$} //= DBIx::Simple->connect($ENV{OOF_DSN} // 'dbi:Pg:');
 
-       my $data = $db->select(products => '*', {}, 'product')->hashes;
+       my $data = $db{$$}->select(products => '*', {}, 'product')->hashes;
        my $tree = $form->clone;
        $tree->find('tbody')->find('tr')->iter3($data, \&form_table_row);
 
@@ -81,7 +93,7 @@ sub continue_table_row {
 
 sub continue_app {
        my ($env) = @_;
-       $db //= DBIx::Simple->connect($ENV{OOF_DSN} // 'dbi:Pg:');
+       $db{$$} //= DBIx::Simple->connect($ENV{OOF_DSN} // 'dbi:Pg:');
        my $tree = $continue->clone;
        my $req = Plack::Request->new($env);
        my $params = $req->body_parameters;
@@ -90,7 +102,7 @@ sub continue_app {
        for (sort keys %$params) {
                next unless /^quant/;
                next unless $params->{$_};
-               my $data = $db->select(products => '*', {product => substr $_, 5})->hash;
+               my $data = $db{$$}->select(products => '*', {product => substr $_, 5})->hash;
                $data->{quantity} = $params->{$_};
                if ($data->{stock} == 0) {
                        push @notes, 'Item is out of stock and was removed from order: '.$data->{title};
@@ -106,13 +118,15 @@ sub continue_app {
                push @data, $data
        }
 
+       return [500, ['Content-type' => 'text/plain'], ['Error: no items in order.']] unless $quant;
+
        $tree->fid('subtotal')->replace_content(stringify_money $total);
        my $dvalue;
        if ($params->{discount}) {
-               my $discount = $db->select(discounts => '*', {discount => $params->{discount}})->hash;
+               my $discount = $db{$$}->select(discounts => '*', {discount => $params->{discount}})->hash;
                if (!defined $discount) {
                        push @notes, 'Discount code incorrect. No discount applied.'
-               } elsif ($db->select(orders => 'COUNT(*)', {discount => $params->{discount}})->list) {
+               } elsif ($db{$$}->select(orders => 'COUNT(*)', {discount => $params->{discount}})->list) {
                        push @notes, 'Discount code already used once. No discount applied.'
                } else {
                        $dvalue = int (0.5 + $discount->{fraction} * $total) if $discount->{fraction};
@@ -141,31 +155,92 @@ sub continue_app {
 
 sub order_app {
        my ($env) = @_;
-       $db //= DBIx::Simple->connect($ENV{OOF_DSN} // 'dbi:Pg:');
+       $db{$$} //= DBIx::Simple->connect($ENV{OOF_DSN} // 'dbi:Pg:');
        my $tree = $order->clone;
        my $req = Plack::Request->new($env);
-       my $id = sprintf "%X", time; # Not good enough!
-
-       $db->begin_work;
-       $db->insert(orders => {id => $id, %{$req->body_parameters}});
-       my $products = decode_json $req->body_parameters->{products};
-       for my $prod (@$products) {
-               my $stock = $db->select(products => 'stock', {product => $prod->{product}})->list;
-               die "Not enough of " .$prod->{title}."\n" if $prod->{quantity} > $stock;
-               $db->update(products => {stock => $stock - $prod->{quantity}}, {product => $prod->{product}});
+       my ($id) = $env->{PATH_INFO} =~ m,^/([0-9A-F]+),;
+       if ($id) {
+               my $total = $db{$$}->select(orders => 'total', {id => $id})->list;
+               $tree->fid('orderid')->replace_content($id);
+               $tree->look_down(name => 'order')->attr(value => $id);
+               $tree->fid('total')->replace_content(stringify_money $total);
+               $tree->find('script')->attr('data-amount', $total);
+               return [200, ['Content-type' => 'text/html; charset=utf-8'], [$tree->as_HTML]]
+       } else {
+               my %parms = %{$req->body_parameters};
+               my $id = sprintf "%X%04X", time, $$;
+               my $err;
+               try {
+                       $db{$$}->begin_work;
+                       my $products = decode_json $req->body_parameters->{products};
+                       for my $prod (@$products) {
+                               my $stock = $db{$$}->select(products => 'stock', {product => $prod->{product}})->list;
+                               die "Not enough of " .$prod->{title}."\n" if $prod->{quantity} > $stock;
+                               $db{$$}->update(products => {stock => $stock - $prod->{quantity}}, {product => $prod->{product}});
+                       }
+                       $db{$$}->insert(orders => {id => $id, %parms});
+                       $db{$$}->commit;
+               } catch {
+                       $db{$$}->rollback;
+                       $err = [500, ['Content-type', 'text/plain'], ["Error: $_"]]
+               };
+               return $err if $err;
+               return [303, [Location => "/order/$id"], []]
        }
-       $db->commit;
+}
+
+sub details_list_element {
+       my ($data, $li) = @_;
+       $li->find('a')->attr(href => "/$data");
+       my $thumb = $data =~ s/fullpics/thumbs/r;
+       $thumb = $data unless -f $thumb;
+       $li->find('img')->attr(src => "/$thumb");
+}
+
+sub details_app {
+       my ($env) = @_;
+       $db{$$} //= DBIx::Simple->connect($ENV{OOF_DSN} // 'dbi:Pg:');
+       my $tree = $details->clone;
+       my ($id) = $env->{PATH_INFO} =~ m,^/(\d+),;
+       my $title = $db{$$}->select(products => 'title', {product => $id})->list;
+       my @pics = <static/fullpics/$id-*>;
+       my $slug = make_slug $title;
+       $tree->find('title')->replace_content("Pictures of $title");
+       $tree->find('h2')->replace_content($title);
+       $tree->look_down(rel => 'canonical')->attr(href => "/details/$id/$slug");
+       $tree->fid('pictures')->iter3(\@pics, \&details_list_element);
 
-       $tree->fid('orderid')->replace_content($id);
        [200, ['Content-type' => 'text/html; charset=utf-8'], [$tree->as_HTML]]
 }
 
+sub pay_app {
+       my ($env) = @_;
+       my $req = Plack::Request->new($env);
+       $db{$$} //= DBIx::Simple->connect($ENV{OOF_DSN} // 'dbi:Pg:');
+       my $order = $req->body_parameters->{order};
+       my $token = $req->body_parameters->{stripeToken};
+       return [500, ['Content-type' => 'text/html; charset=utf-8'], ['No token received, payment did not succeed.']] unless $token;
+       $db{$$}->update(orders => {stripe_token => $token}, {id => $order});
+       [200, ['Content-type' => 'text/html; charset=utf-8'], [$pay->as_HTML]];
+}
+
 sub app {
+       my $footer = read_file 'tmpl/footer.html';
        builder {
+               enable sub {
+                       my $app = shift;
+                       sub {
+                               my $res = $app->(@_);
+                               push @{$res->[2]}, $footer;
+                               $res;
+                       }
+               };
                mount '/' => sub { [301, [Location => '/form'], []] };
                mount '/form'     => \&form_app;
                mount '/continue' => \&continue_app;
                mount '/order'    => \&order_app;
+               mount '/details'  => \&details_app;
+               mount '/pay'      => \&pay_app;
        }
 }
 
diff --git a/static/Eligible-Bold.ttf b/static/Eligible-Bold.ttf
new file mode 100644 (file)
index 0000000..f7aef59
Binary files /dev/null and b/static/Eligible-Bold.ttf differ
diff --git a/static/Eligible-Regular.ttf b/static/Eligible-Regular.ttf
new file mode 100644 (file)
index 0000000..7f4fd07
Binary files /dev/null and b/static/Eligible-Regular.ttf differ
diff --git a/static/Gravity-Bold.otf b/static/Gravity-Bold.otf
new file mode 100644 (file)
index 0000000..4077358
Binary files /dev/null and b/static/Gravity-Bold.otf differ
index ba7af922c29f0e2100fd7a62ed328881787ccde3..dfc299fb23317dfd54960c04e747da5df2391b6f 100644 (file)
@@ -1,16 +1,43 @@
 @font-face {
-       font-family: "Gravity Ultra Light";
+       font-family: "Gravity";
+       font-weight: 200;
        src: url("/static/Gravity-UltraLight.otf");
 }
 
+@font-face {
+       font-family: "Gravity";
+       font-weight: 700;
+       src: url("/static/Gravity-Bold.otf");
+}
+
+@font-face {
+       font-family: "Eligible";
+       font-weight: 400;
+       src: url("/static/Eligible-Regular.ttf");
+}
+
+@font-face {
+       font-family: "Eligible";
+       font-weight: 700;
+       src: url("/static/Eligible-Bold.ttf");
+}
+
+html, body{
+       margin: 0;
+}
+
 body {
+       font-family: "Eligible";
        background: url("/static/pattern.png");
-       padding: 0.3em 1em;
+       padding: 1em;
+       padding-top: 0.2em;
        line-height: 1.4;
+       font-size: 1.1em;
 }
 
 #title {
-       font-family: "Gravity Ultra Light";
+       font-family: "Gravity";
+       font-weight: 200;
        text-align: center;
        font-size: 5em;
        font-weight: normal;
@@ -18,6 +45,18 @@ body {
        margin-bottom: 5px;
 }
 
+#subtitle {
+       display: block;
+       text-align: center;
+}
+
+h2 {
+       font-family: "Gravity";
+       font-weight: 700;
+       font-size: 2em;
+       margin: 0.0em 0;
+}
+
 #items, #order {
        width: 100%;
 }
@@ -35,15 +74,16 @@ a.title {
        text-decoration: none;
 }
 
-#continue, #place_order{
+#continue, #place_order {
        font-size: 1.2em;
+       margin: auto;
+       cursor: pointer;
        font-weight: bold;
        padding: 1em;
        background-color: lightgreen;
        border-radius: 2em;
        border-style: solid;
        border-width: medium;
-       margin: auto;
        display: block;
 }
 
@@ -53,4 +93,15 @@ a.title {
 
 #total {
        font-weight: bold;
+}
+
+#pictures li {
+       list-style-type: none;
+       margin: 0;
+}
+
+footer {
+       padding-top: 1em;
+       font-size: 0.8em;
+       text-align: center;
 }
\ No newline at end of file
index 9f323426306dd9085e8ce29629609352c9e49251..374989a284c6adab7e5e3f0ecee9474681131293 100644 (file)
@@ -4,6 +4,7 @@
 <title>Order details</title>
 
 <h1 id="title">ledparts4you</h1>
+<div id="subtitle">Issues/Questions? Contact us at <a href="mailto:orders@ledparts4you.uk.to">orders@ledparts4you.uk.to</a></div>
 
 <ul id="notes"><li>Note</ul>
 
 <label>Address line 4 <em>(optional)</em><br><input name="address4" type="text" maxlength="32"></label><br>
 <label>Safe place <em>(optional)</em><br><input name="safe_place" type="text" maxlength="32"></label><br>
 <label>Delivery instructions <em>(optional)</em><br><input name="instructions" type="text" maxlength="32"></label><br>
+<br>
+You can pay with bank transfer or credit/debit card via Stripe.<br>
+<input type="submit" value="Place order" id="place_order">
 
 <input type="hidden" name="discount">
 <input type="hidden" name="products">
 <input type="hidden" name="total">
-<input type="submit" value="Place order" id="place_order">
 </form>
diff --git a/tmpl/details.html b/tmpl/details.html
new file mode 100644 (file)
index 0000000..bc91e15
--- /dev/null
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="stylesheet" href="/static/style.css">
+<link rel="canonical" href="https://example.org">
+<title>Pictures of an item</title>
+
+<h1 id="title">ledparts4you</h1>
+<div id="subtitle">Issues/Questions? Contact us at <a href="mailto:orders@ledparts4you.uk.to">orders@ledparts4you.uk.to</a></div>
+
+<h2>Item</h2>
+<ul id="pictures"><li><a href="https://example.org"><img src="https:/example.org"></a></ul>
+
+Go to <a href="/">order form</a>.
diff --git a/tmpl/footer.html b/tmpl/footer.html
new file mode 100644 (file)
index 0000000..8cfd15a
--- /dev/null
@@ -0,0 +1,3 @@
+<footer>
+Thanks: <a href="https://freedns.afraid.org/">Free DNS</a> | <a href="http://subtlepatterns.com/">Subtle Patterns</a> | <a href="http://www.dafont.com/gravity.font">Gravity font</a> by Vincenzo Vuono | <a href="http://www.dafont.com/eligible.font">Eligible font</a> by Jérémie Dupuis.
+</footer>
index b5da6fce931c25efaeb53069baa0d4d25d756d90..268748552a799ec2a81baaabb057ce874fd4d756 100644 (file)
@@ -4,12 +4,13 @@
 <title>Order form</title>
 
 <h1 id="title">ledparts4you</h1>
+<div id="subtitle">Issues/Questions? Contact us at <a href="mailto:orders@ledparts4you.uk.to">orders@ledparts4you.uk.to</a></div>
 
 <div id="info"></div>
 <form action="continue" method="POST">
 <table id="items">
 <thead><tr><th>Item<th>Price<th>Stock<th>Quantity</thead>
-<tbody><tr><td class="item"><a href="#" class="title"></a><br><span class="subtitle"></span><td class="price"><td class="stock"><td><input class="quantity" type="number" min="0" value="0"></tbody>
+<tbody><tr><td class="item"><a target="lp4y_pics" href="#" class="title"></a><br><span class="subtitle"></span><td class="price"><td class="stock"><td><input class="quantity" type="number" min="0" value="0"></tbody>
 </table>
 
 <label>Discount code (optional)<br><input name="discount" type="text"></label><br>
index f105b143fb9a6a76e9fc978336f3cddeb29cc8c5..977fbdc534fe283261d72b4c4c318761e76681e2 100644 (file)
@@ -4,6 +4,20 @@
 <title>Order placed</title>
 
 <h1 id="title">ledparts4you</h1>
+<div id="subtitle">Issues/Questions? Contact us at <a href="mailto:orders@ledparts4you.uk.to">orders@ledparts4you.uk.to</a></div>
+
+<h2>Order placed - continue to payment</h2>
+<p>Order <strong id="orderid">FAKEID</strong> has been placed successfully. The items will be dispatched within 2 business days of receiving payment. You will receive an email with tracking information from Hermes when the order has been dispatched.
+
+<p>Use one of the following methods to pay for your order.
+
+<h2>Pay with bank transfer <small>(recommended)</small></h2>
+Transfer <strong id="total">&pound;xx</strong> to Marius Gavrilescu (sort code 77-66-72, account number 01496860), using your full name or postcode as payment reference.
+
+<h2>Pay with card</h2>
+<form method="POST" action="/pay">
+<input type="hidden" name="order">
+<script src="https://checkout.stripe.com/checkout.js" class="stripe-button" data-key="pk_live_lT2Fq70reqw2wlgUKiDrMv3H" data-name="ledparts4you" data-description="LED parts" data-currency="GBP" data-zip-code="true" data-locale="auto"/>
+</form>
+</div>
 
-<h2>Success</h2>
-Order <strong id="orderid">FAKEID</strong> has been placed successfully. You will receive an email when the order has been dispatched.
diff --git a/tmpl/pay.html b/tmpl/pay.html
new file mode 100644 (file)
index 0000000..89850cb
--- /dev/null
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="stylesheet" href="/static/style.css">
+<title>Card payment information</title>
+
+<h1 id="title">ledparts4you</h1>
+<div id="subtitle">Issues/Questions? Contact us at <a href="mailto:orders@ledparts4you.uk.to">orders@ledparts4you.uk.to</a></div>
+
+Card information received by Stripe. You will be charged when the order is dispatched. If we have issues charging your card we will contact you via email.
This page took 0.02613 seconds and 4 git commands to generate.