-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5940ae4
commit ae0567a
Showing
3 changed files
with
382 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,307 @@ | ||
@using Newtonsoft.Json.Linq | ||
<form class="d-flex flex-column gap-4" @onsubmit="Submit"> | ||
<div ref="display" class="d-flex flex-column align-items-center px-4 mb-auto"> | ||
<div class="fw-semibold text-muted" id="Currency">@CurrencyCode</div> | ||
<div class="fw-bold lh-sm" style="font-size:@(FontSize + "px")" id="Amount">@FormatCurrency(GetTotal(), false)</div> | ||
<div class="text-muted text-center mt-2" id="Calculation">@Calculation(Model)</div> | ||
</div> | ||
@if (IsDiscountEnabled || IsTipEnabled) | ||
{ | ||
<div id="ModeTabs" class="tab-content mb-n2"> | ||
@if (IsDiscountEnabled) | ||
{ | ||
<div class="tab-pane fade px-2 @(Mode == InputMode.Discount ? "show active" : "")" role="tabpanel" aria-labelledby="ModeTablist-Discount"> | ||
<div class="h4 fw-semibold text-muted text-center" id="Discount"> | ||
<span class="h3 text-body me-1">@Model.DiscountPercent%</span> discount | ||
</div> | ||
</div> | ||
} | ||
@if (IsTipEnabled) | ||
{ | ||
var tip = GetTip(); | ||
<div class="tab-pane fade px-2 @(Mode == InputMode.Tip ? "show active" : "")" role="tabpanel" aria-labelledby="ModeTablist-Tip"> | ||
<div class="btcpay-pills d-flex flex-wrap align-items-center justify-content-center gap-2"> | ||
@if (CustomTipPercentages != null) | ||
{ | ||
<button | ||
id="Tip-Custom" | ||
type="button" | ||
class="btcpay-pill @(Model.TipPercent == null ? "active" : "")" | ||
@onclick="() => Model.TipPercent = null"> | ||
@(Model.Tip is > 0 ? FormatCurrency(Model.Tip.Value, true) : "Custom") | ||
</button> | ||
@foreach(var percentage in CustomTipPercentages) | ||
{ | ||
<button | ||
type="button" | ||
id="Tip-@percentage" | ||
class="btcpay-pill @(Model.TipPercent == percentage ? "active" : "")" | ||
@onclick="() => Model.TipPercent = percentage"> | ||
@percentage% | ||
</button> | ||
} | ||
} | ||
else | ||
{ | ||
<div class="h5 fw-semibold text-muted text-center"> | ||
Amount@(tip > 0 ? $": {FormatCurrency(tip, true)}" : "") | ||
</div> | ||
} | ||
</div> | ||
</div> | ||
} | ||
</div> | ||
<div id="ModeTablist" class="nav btcpay-pills align-items-center justify-content-center mb-n2 pb-1" role="tablist"> | ||
@foreach (var mode in GetModes()) | ||
{ | ||
<input id="ModeTablist-@mode" name="mode" value="@mode" type="radio" role="tab" | ||
aria-controls="Mode-@mode" aria-selected="@(Mode == mode ? "true" : "false")" | ||
data-bs-toggle="pill" data-bs-target="#Mode-@mode" | ||
checked="@(Mode == mode)" | ||
disabled="@(Mode != InputMode.Amount && GetAmount() is 0)" | ||
@onclick="() => Mode = mode"> | ||
<label for="ModeTablist-@mode">@mode</label> | ||
} | ||
</div> | ||
} | ||
<div class="keypad"> | ||
@foreach (var key in Keys) | ||
{ | ||
<button disabled="@(key == '+' && Mode != InputMode.Amount)" @onclick="@(e => KeyPress(key))" @onclick:preventDefault @ondblclick="@(e => DoublePress(key))" type="button" class="btn btn-secondary btn-lg" data-key="@key">@key</button> | ||
} | ||
</div> | ||
<button class="btn btn-lg btn-primary mx-3" type="submit" disabled="@IsSubmitting" id="pay-button"> | ||
@if (IsSubmitting) | ||
{ | ||
<div class="spinner-border spinner-border-sm" role="status"> | ||
<span class="sr-only">Loading...</span> | ||
</div> | ||
} | ||
else | ||
{ | ||
<span>Charge</span> | ||
} | ||
</button> | ||
</form> | ||
|
||
@code { | ||
[Parameter] | ||
public string CurrencyCode { get; set; } | ||
[Parameter] | ||
public string CurrencySymbol { get; set; } | ||
[Parameter] | ||
public int CurrencyDivisibility { get; set; } | ||
[Parameter] | ||
public bool IsDiscountEnabled { get; set; } | ||
[Parameter] | ||
public bool IsTipEnabled { get; set; } | ||
[Parameter] | ||
public int[] CustomTipPercentages { get; set; } | ||
|
||
private bool IsSubmitting { get; set; } | ||
|
||
const int DefaultFontSize = 64; | ||
static char[] Keys = { '1', '2', '3', '4', '5', '6', '7', '8', '9', 'C', '0', '+' }; | ||
public int FontSize { get; set; } = DefaultFontSize; | ||
|
||
public enum InputMode | ||
{ | ||
Amount, | ||
Discount, | ||
Tip | ||
} | ||
|
||
public InputMode Mode { get; set; } = InputMode.Amount; | ||
|
||
public class KeypadModel | ||
{ | ||
public List<decimal> Amounts { get; set; } = new (); | ||
public int? DiscountPercent { get; set; } | ||
public int? TipPercent { get; set; } | ||
public decimal? Tip { get; set; } | ||
} | ||
|
||
private KeypadModel Model { get; set; } | ||
|
||
protected override void OnInitialized() | ||
{ | ||
Model ??= new KeypadModel(); | ||
} | ||
|
||
private void Submit() | ||
{ | ||
IsSubmitting = true; | ||
} | ||
|
||
public void KeyPress(char key = '1') | ||
{ | ||
if (Mode == InputMode.Amount) { | ||
var lastIndex = Model.Amounts.Count - 1; | ||
var lastAmount = Model.Amounts[lastIndex]; | ||
if (key == 'C') { | ||
if (lastAmount == 0 && lastIndex == 0) { | ||
// clear completely | ||
Clear(); | ||
} else if (lastAmount == 0) { | ||
// remove latest value | ||
Model.Amounts.RemoveAt(lastIndex); | ||
} else { | ||
// clear latest value | ||
Model.Amounts[lastIndex] = 0; | ||
} | ||
} else if (key == '+' && lastAmount != 0) { | ||
Model.Amounts.Add(0); | ||
} else { // Is a digit | ||
Model.Amounts[lastIndex] = ApplyKeyToValue(key, lastAmount, CurrencyDivisibility); | ||
} | ||
} else { | ||
if (key == 'C') { | ||
if (Mode == InputMode.Tip) | ||
{ | ||
Model.Tip = null; | ||
Model.TipPercent = null; | ||
} | ||
else | ||
{ | ||
Model.DiscountPercent = null; | ||
} | ||
} else { | ||
var divisibility = Mode == InputMode.Tip ? CurrencyDivisibility : 0; | ||
if (Mode == InputMode.Tip) | ||
{ | ||
Model.Tip = ApplyKeyToValue(key, Model.Tip ?? 0, divisibility); | ||
Model.TipPercent = null; | ||
} | ||
else | ||
{ | ||
Model.DiscountPercent = (int)ApplyKeyToValue(key, Model.DiscountPercent ?? 0, divisibility); | ||
} | ||
} | ||
} | ||
} | ||
|
||
public decimal ApplyKeyToValue(char key, decimal value, int divisibility) | ||
{ | ||
var val = value is 0 ? "" : Formatted(value, divisibility); | ||
val = (val + key) | ||
.Replace(".", "") | ||
.PadLeft(divisibility, '0'); | ||
//.Replace(new Regex("(\\d*)(\\d{{divisibility}})", "\\1.\\2"); | ||
return decimal.Parse(val); | ||
} | ||
|
||
public void DoublePress(char key) | ||
{ | ||
if (key == 'C') { | ||
Clear(); | ||
} | ||
} | ||
|
||
public void Clear() | ||
{ | ||
Model.Amounts.Clear(); | ||
Model.DiscountPercent = null; | ||
Model.TipPercent = null; | ||
Model.Tip = null; | ||
Mode = InputMode.Amount; | ||
} | ||
|
||
private JObject GetData() | ||
{ | ||
var data = new JObject | ||
{ | ||
["subTotal"] = GetAmount(), | ||
["total"] = GetTotal(), | ||
}; | ||
|
||
var discount = GetDiscount(); | ||
if (discount > 0) | ||
{ | ||
data["discount"] = discount; | ||
} | ||
if (Model.DiscountPercent is > 0) | ||
{ | ||
data["discountPercentage"] = Model.DiscountPercent; | ||
} | ||
|
||
var tip = GetTip(); | ||
if (tip > 0) | ||
{ | ||
data["tip"] = tip; | ||
} | ||
if (Model.TipPercent != null) | ||
{ | ||
data["tipPercentage"] = Model.TipPercent; | ||
} | ||
return data; | ||
} | ||
|
||
private List<InputMode> GetModes() | ||
{ | ||
var modes = new List<InputMode> { InputMode.Amount }; | ||
if (IsDiscountEnabled) modes.Add(InputMode.Discount); | ||
if (IsTipEnabled) modes.Add(InputMode.Tip); | ||
return modes; | ||
} | ||
|
||
private decimal GetAmount() | ||
{ | ||
return Model.Amounts.Count > 0 ? Model.Amounts.Sum() : Model.Amounts.FirstOrDefault(); | ||
} | ||
|
||
private decimal GetDiscount() | ||
{ | ||
var amount = GetAmount(); | ||
return amount > 0 && Model.DiscountPercent is > 0 | ||
? Math.Round(amount * (Model.DiscountPercent.Value / 100.0m), CurrencyDivisibility) | ||
: 0; | ||
} | ||
|
||
private decimal GetTip() | ||
{ | ||
if (Model.TipPercent is > 0) { | ||
var amount = GetAmount() - GetDiscount(); | ||
return Math.Round(amount * (Model.TipPercent.Value / 100.0m), CurrencyDivisibility); | ||
} | ||
return Model.Tip is > 0 ? Math.Round(Model.Tip.Value, CurrencyDivisibility) : 0.0m; | ||
} | ||
|
||
private decimal GetTotal() | ||
{ | ||
return GetAmount() - GetDiscount() + GetTip(); | ||
} | ||
|
||
private string Calculation(KeypadModel model) | ||
{ | ||
if (model.Amounts.Count < 2 && model.DiscountPercent is not > 0 && !model.Tip.HasValue) return null; | ||
var calc = string.Join(" + ", model.Amounts.Select(amt => FormatCurrency(amt, true))); | ||
var discount = GetDiscount(); | ||
if (discount > 0) calc += $" - {FormatCurrency(discount, true)} (${model.DiscountPercent}%)"; | ||
var tip = GetTip(); | ||
if (model.Tip > 0) calc += $" + ${FormatCurrency(tip, true)}"; | ||
if (model.TipPercent > 0) calc += $" (${model.TipPercent}%)"; | ||
return calc; | ||
} | ||
|
||
private string FormatCurrency(decimal value, bool withSymbol) | ||
{ | ||
if (CurrencyCode is "BTC" or "SATS") return FormatCrypto(value, withSymbol); | ||
try { | ||
var symbol = withSymbol ? $" {CurrencySymbol ?? CurrencyCode}" : ""; | ||
return $"{Formatted(value, CurrencyDivisibility)}{symbol}"; | ||
} | ||
catch (Exception) | ||
{ | ||
return FormatCrypto(value, withSymbol); | ||
} | ||
} | ||
|
||
private string FormatCrypto(decimal value, bool withSymbol) { | ||
var symbol = withSymbol ? $" {CurrencySymbol ?? CurrencyCode}" : ""; | ||
return $"{Formatted(value, CurrencyDivisibility)}{symbol}"; | ||
} | ||
|
||
private string Formatted(decimal value, int divisibility) { | ||
return string.Format($"{{0:0.{new string('0', divisibility)}}}", value); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
|
||
/* modes */ | ||
#ModeTabs { | ||
min-height: 2.75rem; | ||
} | ||
|
||
/* keypad */ | ||
.keypad { | ||
display: grid; | ||
grid-template-columns: repeat(3, 1fr); | ||
} | ||
.keypad .btn { | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
padding: 0; | ||
position: relative; | ||
border-radius: 0; | ||
font-weight: var(--btcpay-font-weight-semibold); | ||
font-size: 24px; | ||
min-height: 3.5rem; | ||
height: 8vh; | ||
max-height: 6rem; | ||
color: var(--btcpay-body-text); | ||
} | ||
.keypad .btn[data-key="+"] { | ||
font-size: 2.25em; | ||
} | ||
.btcpay-pills label, | ||
.btn-secondary.rounded-pill { | ||
padding-left: 1rem; | ||
padding-right: 1rem; | ||
} | ||
|
||
/* make borders collapse by shifting rows and columns by 1px */ | ||
/* second column */ | ||
.keypad .btn:nth-child(3n-1) { | ||
margin-left: -1px; | ||
} | ||
/* third column */ | ||
.keypad .btn:nth-child(3n) { | ||
margin-left: -1px; | ||
} | ||
/* from second row downwards */ | ||
.keypad .btn:nth-child(n+4) { | ||
margin-top: -1px; | ||
} | ||
/* ensure highlighted button is topmost */ | ||
.keypad .btn:hover, | ||
.keypad .btn:focus, | ||
.keypad .btn:active { | ||
z-index: 1; | ||
} | ||
|
||
#Calculation { | ||
min-height: 1.5rem; | ||
} | ||
|
||
/* fix sticky hover effect on mobile browsers */ | ||
@media (hover: none) { | ||
.keypad .btn-secondary:hover, | ||
.actions .btn-secondary:hover { | ||
border-color: var(--btcpay-secondary-border-active) !important; | ||
} | ||
} |
Oops, something went wrong.