From ee9d459966e571d83a09f16792f677a3c1edca39 Mon Sep 17 00:00:00 2001 From: PJ Hampton Date: Fri, 23 Jun 2017 22:12:52 +0100 Subject: [PATCH] Tips: Add interactivity (#4155) * initial commit. * Fixed NaN issue with bill amount. * Finish up tips. * Fixed markup. * Remove mathjs as dep * Finishing touches... * Tweak design. * Add class * Add simplified css * overhaul ui * Add total per person. * Add secondary text * Update input class. * Add further bindings. * Removed rouge file. --- lib/DDG/Goodie/Tips.pm | 87 ++++++++++++++----------- share/goodie/tips/content.handlebars | 33 ++++++++++ share/goodie/tips/tips.css | 92 ++++++++++++++++++++++++++ share/goodie/tips/tips.js | 96 ++++++++++++++++++++++++++++ t/Tips.t | 62 +++++++++--------- 5 files changed, 301 insertions(+), 69 deletions(-) create mode 100644 share/goodie/tips/content.handlebars create mode 100644 share/goodie/tips/tips.css create mode 100644 share/goodie/tips/tips.js diff --git a/lib/DDG/Goodie/Tips.pm b/lib/DDG/Goodie/Tips.pm index ad0f086ab..cc78233b8 100644 --- a/lib/DDG/Goodie/Tips.pm +++ b/lib/DDG/Goodie/Tips.pm @@ -1,12 +1,14 @@ package DDG::Goodie::Tips; -# ABSTRACT: calculate a tip/tax on a bill or a general percentage +# ABSTRACT: Calculates a tip on a bill or a general percentage use strict; use DDG::Goodie; with 'DDG::GoodieRole::NumberStyler'; -# Yes, 'of' is very generic, the guard should kick back false positives very quickly. -triggers any => 'tip', 'tips', 'of', 'tax'; + +my @generic_trigs = ('tip calculator', 'calculate tip', 'tips calculator', 'calculate tips', 'bill tip', 'tip cost'); +triggers any => @generic_trigs; +triggers any => 'tip', 'tips', 'of'; zci answer_type => 'tip'; zci is_cached => 1; @@ -14,47 +16,54 @@ zci is_cached => 1; my $number_re = number_style_regex(); handle query_lc => sub { - return unless (/^(?

$number_re)(?: ?%| percent) (?:(?(?tip|tax) (?:on|for|of))|of)(?: an?)? (?[\$\-]?)(?$number_re)(?: bill)?$/); - - my ($p, $num, $sign) = ($+{'p'}, $+{'num'}, $+{'sign'}); - my $style = number_style_for($p, $num); - $p = $style->for_computation($p) / 100; - $num = $style->for_computation($num); - my $t = $p * $num; - - if ($+{'do_tip'}) { - my $subtotal = $style->for_display(sprintf "%.2f", $num); - my $tax_or_tip = ucfirst($+{'tax_or_tip'}); - my $tax_or_tip_value = $style->for_display(sprintf "%.2f", $t); - my $total = $style->for_display(sprintf "%.2f", $num + $t); - - my $tax_or_tip_answer = "Subtotal: \$$subtotal; $tax_or_tip: \$$tax_or_tip_value; Total: \$$total"; - return $tax_or_tip_answer, - structured_answer => { - data => { - title => "$tax_or_tip_answer", - }, - templates => { - group => 'text' - } - }; - } - $t = sprintf "%.2f", $t if ($sign eq '$'); # Maybe this makes cents. - my $calculated_answer = $style->for_display($t); - my $percentage = $style->for_display($p * 100); - my $number = $style->for_display($num); - my $percent_answer = "$sign$calculated_answer is $percentage% of $sign$number"; - - return $percent_answer, - structured_answer => { + # sets up the vanilla UI with default values + # no values should be pased to the front-end + if($_ ~~ @generic_trigs) { + return '', structured_answer => { data => { - title => "$percent_answer" + title => "Tip Calculator", + percentage => '', + bill => '', }, templates => { - group => 'text' + group => 'text', + options => { + content => 'DDH.tips.content' + }, } }; + } + + # The following code handles more verbose and + # complicated queries such as: + # + # - 20% tip on $400 bill + # - 14% tip for a $10 bill + return unless (/^(?

$number_re)(?: ?%| percent) (?:(?(?tip) (?:on|for|of))|of)(?: an?)? (?[\$\-]?)(?$number_re)(?: bill)?$/); + + my ($p, $num) = ($+{'p'}, $+{'num'}); + my $style = number_style_for($p, $num); + $p = $style->for_computation($p); + $num = $style->for_computation($num); + + if ($+{'do_tip'}) { + return '', structured_answer => { + data => { + title => "Tip Calculator", + percentage => "$p", + bill => "$num", + }, + templates => { + group => 'text', + options => { + content => 'DDH.tips.content' + }, + }, + }; + } else { + return; + } }; -1; +1; \ No newline at end of file diff --git a/share/goodie/tips/content.handlebars b/share/goodie/tips/content.handlebars new file mode 100644 index 000000000..f0208092b --- /dev/null +++ b/share/goodie/tips/content.handlebars @@ -0,0 +1,33 @@ +

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+

Tip

+
Per Person
+
-
+
+ +
+
+

Total

+
Per Person
+
-
+
+
diff --git a/share/goodie/tips/tips.css b/share/goodie/tips/tips.css new file mode 100644 index 000000000..95e87e7db --- /dev/null +++ b/share/goodie/tips/tips.css @@ -0,0 +1,92 @@ +.zci--tips label { + font-weight: bold; + font-size: 1.3em; +} + +.zci--tips input { + display: block; + width: 100%; + margin-bottom: 10px; + font-size: 2em; + line-height: 1.5em; + font-weight: bold; + text-align: center; +} + +.zci--tips .tips__col { + display: inline-block; +} + +.zci--tips .tips__col--left { + border-right: 1px solid #e0e0e0; + padding-right: 2em; + margin-right: 2em; +} + +.zci--tips .tips__block { + margin-bottom: 1em; +} + +.zci--tips .tips__label { + display: inline-block; +} + +.zci--tips .tips__label h4 { + padding-bottom: 0px; +} + +.zci--tips .tips__label h4 + div { + font-size: 1em; +} + +.zci--tips .tips__block .tips__amt { + font-size: 3em; +} + +/* Mobile */ +.is-mobile .zci--tips .tips__col--left { + border-right: none; + padding-right: 0; +} + +.is-mobile .zci--tips .tips__col--right { + width: 100%; + border-top: 1px solid #e0e0e0; + margin-top: 1em; + padding-top: 1em; + padding-left: 0; +} + +.is-mobile .zci--tips input { + font-size: 1.6em; + line-height: 1.7em; + width: 100%; +} + +.is-mobile .zci--tips .tips__block { + margin-bottom: 0; +} + +.is-mobile .zci--tips .tips__block .tips__label { + width: 30%; +} + +.is-mobile .zci--tips .tips__block .tips__label h4 { + font-size: 3em; + padding-top: 0; +} + +.is-mobile .zci--tips .tips__block .tips__label h4.tips__label--pp { + font-size: 1.3em; +} + +.is-mobile .zci--tips .tips__block { + display: inline-block; + width: 100% +} + +.is-mobile .zci--tips .tips__amt { + display: inline-block; + width: 70%; + text-align: right; +} diff --git a/share/goodie/tips/tips.js b/share/goodie/tips/tips.js new file mode 100644 index 000000000..f8683dca5 --- /dev/null +++ b/share/goodie/tips/tips.js @@ -0,0 +1,96 @@ +DDH.tips = DDH.tips || {}; + +(function(DDH) { + "use strict"; + + var initialized = false; + var $dom, $inputs, $bill_input, $bill_tip, $bill_people, $tip_label, $total_label, $tip, $total, $tips_pp, $tips_labels; + + /* + * setUpSelectors + * + * Sets up the jQuery selectors when the IA is built + */ + function setUpSelectors() { + $dom = $(".zci--tips"); + + // the inputs + $inputs = $dom.find('input'); + $bill_input = $dom.find("#bill_input"); + $bill_tip = $dom.find("#bill_tip"); + $bill_people = $dom.find("#bill_people"); + + // the display labels + $tip_label = $dom.find("#tip_label"); + $total_label = $dom.find("#total_label"); + $tip = $dom.find("#tip"); + $total = $dom.find("#total"); + $tips_pp = $dom.find(".tips__pp"); + $tips_labels = $dom.find(".tips__label h4"); + } + + /** + * calculateTip + * + * Calculates the tip and sets the display + */ + function calculateTip() { + var bill_input = $bill_input.val(); + var bill_tip = $bill_tip.val(); + var bill_people = $bill_people.val(); + + if(bill_input === "") { + bill_input = 0; + } + + var tip = bill_input * (bill_tip / 100); + var tip_pp = tip / parseInt(bill_people); + var total = parseFloat(bill_input) + tip; + var total_pp = total / parseInt(bill_people); + + if(bill_people > 1) { + $tips_pp.removeClass('hide'); + $tips_labels.addClass('tips__label--pp'); + $tip.text(tip_pp.toFixed(2)); + $total.text(total_pp.toFixed(2)); + } else { + $tip_label.text("Tip"); + $tips_pp.addClass('hide'); + $tips_labels.removeClass('tips__label--pp'); + $total_label.text("Total"); + $tip.text(tip.toFixed(2)); + $total.text(total.toFixed(2)); + } + + } + + DDH.tips.build = function(ops) { + + // seed the tip calculator with some values + var init_bill = ops.data.bill || "100"; + var init_percentage = ops.data.percentage || "20"; + + return { + onShow: function() { + + if(!initialized) { + setUpSelectors(); + $bill_input.val(init_bill); + $bill_tip.val(init_percentage); + calculateTip() + } + + /** + * Event handlers to update the values when + * keys are pressed + */ + $inputs.bind('keyup click change mousewheel', function(_e) { + calculateTip() + }); + + initialized = true; + } + + }; + }; +})(DDH); diff --git a/t/Tips.t b/t/Tips.t index 5cc04aeae..d134e9a42 100644 --- a/t/Tips.t +++ b/t/Tips.t @@ -10,46 +10,48 @@ zci answer_type => 'tip'; zci is_cached => 1; sub make_structured_answer { - my ($type, $subtotal, $additive, $total) = @_; - - my $title; - if ($type eq 'percentage') { - $title = "$total is $additive% of $subtotal"; - } - else { - $type = ucfirst($type); - $title = "Subtotal: \$$subtotal; $type: \$$additive; Total: \$$total"; - } + my ($percentage, $bill_amount) = @_; - return $title, - structured_answer => { - data => { - title => "$title", + return '', structured_answer => { + data => { + title => "Tip Calculator", + percentage => "$percentage", + bill => "$bill_amount", + }, + templates => { + group => 'text', + options => { + content => 'DDH.tips.content' }, - templates => { - group => 'text' - } - }; + } + }; } sub build_test {test_zci(make_structured_answer(@_))} ddg_goodie_test( [qw( DDG::Goodie::Tips)], - '20% tip on $20' => build_test('tip', '20.00', '4.00', '24.00'), - '20% tip on $20 bill' => build_test('tip', '20.00', '4.00', '24.00'), - '20% tip for a $20 bill' => build_test('tip', '20.00', '4.00', '24.00'), - '20 percent tip on $20' => build_test('tip', '20.00', '4.00', '24.00'), - '20% tip on $21.63' => build_test('tip', '21.63', '4.33', '25.96'), - '20 percent tip for a $20 bill' => build_test('tip', '20.00', '4.00', '24.00'), - '20 percent tip for a $2000 bill' => build_test('tip', '2,000.00', '400.00', '2,400.00'), - '20% tax on $20' => build_test('tax', '20.00', '4.00', '24.00'), - '25 percent of 20000' => build_test('percentage', '20,000', '25', '5,000'), - '2% of 25,000' => build_test('percentage', '25,000', '2', '500'), - '2% of $25,000' => build_test('percentage', '$25,000', '2', '$500.00'), - '2,000% of -2' => build_test('percentage', '-2', '2,000', '-40'), + '20% tip on $20' => build_test('20', '20'), + '20% tip on $20 bill' => build_test('20', '20'), + '20% tip for a $20 bill' => build_test('20', '20'), + '20 percent tip on $20' => build_test('20', '20'), + '20% tip on $21.63' => build_test('20', '21.63'), + '20 percent tip for a $20 bill' => build_test('20', '20'), + '20 percent tip for a $2000 bill' => build_test('20', '2000'), + 'tip calculator' => build_test('', ''), # undef stringified + 'calculate tip' => build_test('', ''), # undef stringified + # queries that will be handled by the calc + '25 percent of 20000' => undef, + '2% of 25,000' => undef, + '2% of $25,000' => undef, + '2,000% of -2' => undef, + '20% tax on $20' => undef, + # random. definately shouldn't trigger this IA 'best of 5' => undef, '4 of 5 dentists' => undef, + 'yo, give me some tips' => undef, + 'tips to save cash' => undef, + 'show me the tip calculator, bro' => undef, ); done_testing;