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.master
parent
9edc2f66bc
commit
ee9d459966
|
@ -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 (/^(?<p>$number_re)(?: ?%| percent) (?:(?<do_tip>(?<tax_or_tip>tip|tax) (?:on|for|of))|of)(?: an?)? (?<sign>[\$\-]?)(?<num>$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 (/^(?<p>$number_re)(?: ?%| percent) (?:(?<do_tip>(?<tax_or_tip>tip) (?:on|for|of))|of)(?: an?)? (?<sign>[\$\-]?)(?<num>$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;
|
|
@ -0,0 +1,33 @@
|
|||
<div class="tips__col tips__col--left forty">
|
||||
|
||||
<div class="tips_input--section">
|
||||
<label for="bill">Bill</label>
|
||||
<input class="frm__input bg-clr--white" type="number" id="bill_input" min="0" step="0.01">
|
||||
</div>
|
||||
|
||||
<div class="tips_input--section">
|
||||
<label for="tip">Tip %</label>
|
||||
<input class="frm__input bg-clr--white" type="number" id="bill_tip" min="0">
|
||||
</div>
|
||||
|
||||
<div class="tips_input--section">
|
||||
<label for="number of people">Number of People</label>
|
||||
<input class="frm__input bg-clr--white" type="number" id="bill_people" value="1" min="0">
|
||||
</div>
|
||||
|
||||
</div><div class="tips__col tips__col--right fifty">
|
||||
|
||||
<div class="tips__block tips__block--tip">
|
||||
<div class="tips__label">
|
||||
<h4 id="tip_label" class="text--primary">Tip</h4>
|
||||
<div class="tips__pp hide text--seccondary t-s">Per Person</div>
|
||||
</div><div id="tip" class="tips__amt text--primary">-</div>
|
||||
</div>
|
||||
|
||||
<div class="tips__block tips__block--total">
|
||||
<div class="tips__label">
|
||||
<h4 id="total_label" class="text--primary">Total</h4>
|
||||
<div class="tips__pp hide text--seccondary t-s">Per Person</div>
|
||||
</div><div id="total" class="tips__amt text--primary t-bold">-</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
62
t/Tips.t
62
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;
|
||||
|
|
Loading…
Reference in New Issue