Compare commits
4 Commits
Kukks-patc
...
v1.7.10
Author | SHA1 | Date | |
---|---|---|---|
8c6fe91c71 | |||
d14ce2a37f | |||
33d272d4b0 | |||
57f5c15670 |
@ -893,7 +893,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(2, windows.Count);
|
||||
s.Driver.SwitchTo().Window(windows[1]);
|
||||
|
||||
Assert.Equal("currently active!",
|
||||
Assert.Equal("Currently active!",
|
||||
s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text);
|
||||
|
||||
s.Driver.Close();
|
||||
|
@ -242,7 +242,10 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
private static async Task<string?> GetMigrationState(ApplicationDbContext postgresContext)
|
||||
{
|
||||
return (await postgresContext.Settings.FromSqlRaw("SELECT \"Id\", \"Value\" FROM \"Settings\" WHERE \"Id\"='MigrationData'").AsNoTracking().FirstOrDefaultAsync())?.Value;
|
||||
var o = (await postgresContext.Settings.FromSqlRaw("SELECT \"Id\", \"Value\" FROM \"Settings\" WHERE \"Id\"='MigrationData'").AsNoTracking().FirstOrDefaultAsync())?.Value;
|
||||
if (o is null)
|
||||
return null;
|
||||
return JObject.Parse(o)["state"]?.Value<string>();
|
||||
}
|
||||
private static async Task SetMigrationState(ApplicationDbContext postgresContext, string migratingFrom, string state)
|
||||
{
|
||||
|
@ -27,10 +27,20 @@
|
||||
{
|
||||
@Safe.Raw($"<style>{Model.EmbeddedCSS}</style>")
|
||||
}
|
||||
|
||||
<style>
|
||||
#crowdfund-main-image {
|
||||
border-radius: var(--btcpay-border-radius);
|
||||
object-fit: cover;
|
||||
max-width: 100%;
|
||||
max-height: 40vh;
|
||||
}
|
||||
#crowdfund-body-description {
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
<vc:ui-extension-point location="crowdfund-head" model="@Model"></vc:ui-extension-point>
|
||||
</head>
|
||||
<body class="min-vh-100">
|
||||
<body class="min-vh-100 p-2">
|
||||
@if (!Model.Enabled)
|
||||
{
|
||||
<div class="alert alert-warning text-center sticky-top mb-0 rounded-0" role="alert">
|
||||
@ -43,31 +53,15 @@
|
||||
}
|
||||
|
||||
<div class="public-page-wrap flex-column container" id="app" @(Model.SimpleDisplay ? "" : "v-cloak")>
|
||||
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
|
||||
<div class="card w-100 p-0 mx-0">
|
||||
@if (!string.IsNullOrEmpty(Model.MainImageUrl))
|
||||
{
|
||||
<img v-if="srvModel.mainImageUrl" src="@Model.MainImageUrl" :src="srvModel.mainImageUrl" alt="@Model.Title" :alt="srvModel.title" class="card-img-top" id="crowdfund-main-image" asp-append-version="true"/>
|
||||
<img v-if="srvModel.mainImageUrl" src="@Model.MainImageUrl" :src="srvModel.mainImageUrl" alt="@Model.Title" :alt="srvModel.title" id="crowdfund-main-image" asp-append-version="true"/>
|
||||
}
|
||||
<div class="d-flex flex-column justify-content-between py-4 px-3 text-center" id="crowdfund-header-container">
|
||||
<h1 v-text="srvModel.title" class="my-3">@Model.Title</h1>
|
||||
@if (!Model.Started && Model.StartDate.HasValue)
|
||||
<div class="d-flex flex-column justify-content-between p-3 text-center" id="crowdfund-header-container">
|
||||
<h1 class="mb-3">@Model.Title</h1>
|
||||
@if (!string.IsNullOrEmpty(Model.Tagline))
|
||||
{
|
||||
<h6 class="text-muted fst-italic" v-if="!started && srvModel.startDate" v-b-tooltip :title="startDate" v-text="`starts in ${startDiff}`" data-test="time-state">
|
||||
starts @TimeZoneInfo.ConvertTimeFromUtc(Model.StartDate.Value, TimeZoneInfo.Local)
|
||||
</h6>
|
||||
}
|
||||
else if (Model.Started && !Model.Ended && Model.EndDate.HasValue)
|
||||
{
|
||||
<h6 class="text-muted fst-italic" v-if="started && !ended && srvModel.endDate" v-b-tooltip :title="endDate" v-text="`ends in ${endDiff}`" data-test="time-state">
|
||||
ends @TimeZoneInfo.ConvertTimeFromUtc(Model.EndDate.Value, TimeZoneInfo.Local)
|
||||
</h6>
|
||||
}
|
||||
else if (Model.Started && !Model.Ended && !Model.EndDate.HasValue)
|
||||
{
|
||||
<h6 class="text-muted fst-italic" v-if="started && !ended && !srvModel.endDate" v-b-tooltip title="No set end date" data-test="time-state">
|
||||
currently active!
|
||||
</h6>
|
||||
<h2 class="h3 mb-3 fw-semibold" v-if="srvModel.tagline" v-text="srvModel.tagline">@Model.Tagline</h2>
|
||||
}
|
||||
@if (Model.TargetAmount.HasValue)
|
||||
{
|
||||
@ -100,11 +94,29 @@
|
||||
}
|
||||
</span>
|
||||
}
|
||||
@if (!Model.Started && Model.StartDate.HasValue)
|
||||
{
|
||||
<h6 class="text-muted fst-italic mt-3" v-if="!started && srvModel.startDate" v-b-tooltip :title="startDate" v-text="`Starts in ${startDiff}`" data-test="time-state">
|
||||
Starts @TimeZoneInfo.ConvertTimeFromUtc(Model.StartDate.Value, TimeZoneInfo.Local)
|
||||
</h6>
|
||||
}
|
||||
else if (Model.Started && !Model.Ended && Model.EndDate.HasValue)
|
||||
{
|
||||
<h6 class="text-muted fst-italic mt-3" v-if="started && !ended && srvModel.endDate" v-b-tooltip :title="endDate" v-text="`Ends in ${endDiff}`" data-test="time-state">
|
||||
Ends @TimeZoneInfo.ConvertTimeFromUtc(Model.EndDate.Value, TimeZoneInfo.Local)
|
||||
</h6>
|
||||
}
|
||||
else if (Model.Started && !Model.Ended && !Model.EndDate.HasValue)
|
||||
{
|
||||
<h6 class="text-muted fst-italic mt-3" v-if="started && !ended && !srvModel.endDate" v-b-tooltip title="No set end date" data-test="time-state">
|
||||
Currently active!
|
||||
</h6>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Model.TargetAmount.HasValue)
|
||||
{
|
||||
<div class="progress rounded-pill mx-3" v-if="srvModel.targetAmount" id="crowdfund-progress-bar">
|
||||
<div class="progress rounded-pill" v-if="srvModel.targetAmount" id="crowdfund-progress-bar">
|
||||
<div class="progress-bar bg-primary"
|
||||
role="progressbar"
|
||||
style="width:@(Model.Info.ProgressPercentage + "%")"
|
||||
@ -130,157 +142,124 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row py-2 text-center crowdfund-stats">
|
||||
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-raised-amount">
|
||||
<h3 v-text="`${raisedAmount} ${targetCurrency}`">@Math.Round(Model.Info.CurrentAmount + Model.Info.CurrentPendingAmount, Model.CurrencyData.Divisibility) @Model.TargetCurrency</h3>
|
||||
<h5 class="text-muted fst-italic mb-0">Raised</h5>
|
||||
<b-tooltip target="crowdfund-body-raised-amount" v-if="paymentStats && paymentStats.length > 0" class="only-for-js">
|
||||
<ul class="p-0 text-uppercase">
|
||||
<li v-for="stat of paymentStats" class="list-unstyled">
|
||||
{{stat.label}} <span v-if="stat.lightning" class="fa fa-bolt"></span> {{stat.value}}
|
||||
</li>
|
||||
<div class="row py-2 text-center crowdfund-stats">
|
||||
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-raised-amount">
|
||||
<h3 v-text="`${raisedAmount} ${targetCurrency}`">@Math.Round(Model.Info.CurrentAmount + Model.Info.CurrentPendingAmount, Model.CurrencyData.Divisibility) @Model.TargetCurrency</h3>
|
||||
<h5 class="text-muted fst-italic mb-0">Raised</h5>
|
||||
<b-tooltip target="crowdfund-body-raised-amount" v-if="paymentStats && paymentStats.length > 0" class="only-for-js">
|
||||
<ul class="p-0 text-uppercase">
|
||||
<li v-for="stat of paymentStats" class="list-unstyled">
|
||||
{{stat.label}} <span v-if="stat.lightning" class="fa fa-bolt"></span> {{stat.value}}
|
||||
</li>
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-goal-raised">
|
||||
<h3 v-text="`${percentageRaisedAmount}%`">@Math.Round(Model.Info.PendingProgressPercentage.GetValueOrDefault(0) + Model.Info.ProgressPercentage.GetValueOrDefault(0))%</h3>
|
||||
<h5 class="text-muted fst-italic mb-0">Of Goal</h5>
|
||||
<b-tooltip target="crowdfund-body-goal-raised" v-if="srvModel.resetEvery !== 'Never'" class="only-for-js">
|
||||
Goal resets every {{srvModel.resetEveryAmount}} {{srvModel.resetEvery}} {{srvModel.resetEveryAmount>1?'s': ''}}
|
||||
</b-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-total-contributors">
|
||||
<h3 v-text="new Intl.NumberFormat().format(srvModel.info.totalContributors)">@Model.Info.TotalContributors</h3>
|
||||
<h5 class="text-muted fst-italic mb-0">Contributors</h5>
|
||||
</div>
|
||||
|
||||
@if (Model.StartDate.HasValue || Model.EndDate.HasValue)
|
||||
{
|
||||
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-campaign-dates">
|
||||
@if (!Model.Started && Model.StartDate.HasValue)
|
||||
{
|
||||
<div v-if="startDiff">
|
||||
<h3 v-text="startDiff">@TimeZoneInfo.ConvertTimeFromUtc(Model.StartDate.Value, TimeZoneInfo.Local)</h3>
|
||||
<h5 class="text-muted fst-italic mb-0" v-text="'Left to start'">Start Date</h5>
|
||||
</div>
|
||||
}
|
||||
else if (Model.Started && !Model.Ended && Model.EndDate.HasValue)
|
||||
{
|
||||
<div v-if="!startDiff && endDiff">
|
||||
<h3 v-text="endDiff">@TimeZoneInfo.ConvertTimeFromUtc(Model.EndDate.Value, TimeZoneInfo.Local)</h3>
|
||||
<h5 class="text-muted fst-italic mb-0" v-text="'Left'">End Date</h5>
|
||||
</div>
|
||||
}
|
||||
else if (Model.Ended)
|
||||
{
|
||||
<div v-if="ended">
|
||||
<h3 class="mb-0">Campaign not active</h3>
|
||||
</div>
|
||||
}
|
||||
<b-tooltip v-if="startDate || endDate" target="crowdfund-body-campaign-dates" class="only-for-js">
|
||||
<ul class="p-0">
|
||||
@if (Model.StartDate.HasValue)
|
||||
{
|
||||
<li v-if="startDate" class="list-unstyled">
|
||||
{{started? "Started" : "Starts"}} {{startDate}}
|
||||
</li>
|
||||
}
|
||||
@if (Model.EndDate.HasValue)
|
||||
{
|
||||
<li v-if="endDate" class="list-unstyled">
|
||||
{{ended? "Ended" : "Ends"}} {{endDate}}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-goal-raised">
|
||||
<h3 v-text="`${percentageRaisedAmount}%`">@Math.Round(Model.Info.PendingProgressPercentage.GetValueOrDefault(0) + Model.Info.ProgressPercentage.GetValueOrDefault(0))%</h3>
|
||||
<h5 class="text-muted fst-italic mb-0">Of Goal</h5>
|
||||
<b-tooltip target="crowdfund-body-goal-raised" v-if="srvModel.resetEvery !== 'Never'" class="only-for-js">
|
||||
Goal resets every {{srvModel.resetEveryAmount}} {{srvModel.resetEvery}} {{srvModel.resetEveryAmount>1?'s': ''}}
|
||||
</b-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-total-contributors">
|
||||
<h3 v-text="new Intl.NumberFormat().format(srvModel.info.totalContributors)">@Model.Info.TotalContributors</h3>
|
||||
<h5 class="text-muted fst-italic mb-0">Contributors</h5>
|
||||
</div>
|
||||
@if (Model.StartDate.HasValue || Model.EndDate.HasValue)
|
||||
{
|
||||
<div class="col-sm border-end p-3 text-center" id="crowdfund-body-campaign-dates">
|
||||
@if (!Model.Started && Model.StartDate.HasValue)
|
||||
{
|
||||
<div v-if="startDiff">
|
||||
<h3 v-text="startDiff">@TimeZoneInfo.ConvertTimeFromUtc(Model.StartDate.Value, TimeZoneInfo.Local)</h3>
|
||||
<h5 class="text-muted fst-italic mb-0" v-text="'Left to start'">Start Date</h5>
|
||||
</div>
|
||||
}
|
||||
else if (Model.Started && !Model.Ended && Model.EndDate.HasValue)
|
||||
{
|
||||
<div v-if="!startDiff && endDiff">
|
||||
<h3 v-text="endDiff">@TimeZoneInfo.ConvertTimeFromUtc(Model.EndDate.Value, TimeZoneInfo.Local)</h3>
|
||||
<h5 class="text-muted fst-italic mb-0" v-text="'Left'">End Date</h5>
|
||||
</div>
|
||||
}
|
||||
else if (Model.Ended)
|
||||
{
|
||||
<div v-if="ended">
|
||||
<h3 class="mb-0">Campaign not active</h3>
|
||||
</div>
|
||||
}
|
||||
<b-tooltip v-if="startDate || endDate" target="crowdfund-body-campaign-dates" class="only-for-js">
|
||||
<ul class="p-0">
|
||||
@if (Model.StartDate.HasValue)
|
||||
{
|
||||
<li v-if="startDate" class="list-unstyled">
|
||||
{{started? "Started" : "Starts"}} {{startDate}}
|
||||
</li>
|
||||
}
|
||||
@if (Model.EndDate.HasValue)
|
||||
{
|
||||
<li v-if="endDate" class="list-unstyled">
|
||||
{{ended? "Ended" : "Ends"}} {{endDate}}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="card-title text-center" id="crowdfund-body-header">
|
||||
@if (!string.IsNullOrEmpty(Model.Tagline))
|
||||
{
|
||||
<h2 class="h3 my-4 fw-normal" v-if="srvModel.tagline" v-text="srvModel.tagline">@Model.Tagline</h2>
|
||||
}
|
||||
<button v-if="active" id="crowdfund-body-header-cta" class="btn btn-lg btn-primary mb-4 py-2 px-5 only-for-js" v-on:click="contributeModalOpen = true">Contribute</button>
|
||||
</div>
|
||||
|
||||
<hr class="w-100"/>
|
||||
|
||||
<template v-if="srvModel.disqusEnabled && srvModel.disqusShortname">
|
||||
<b-tabs>
|
||||
<b-tab title="Details" active>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-8 col-sm-12" id="crowdfund-body-description-container">
|
||||
<div class="card-text overflow-hidden" v-html="srvModel.description" id="crowdfund-body-description">
|
||||
@Safe.Raw(Model.Description)
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12" id="crowdfund-body-contribution-container">
|
||||
<contribute :target-currency="srvModel.targetCurrency"
|
||||
:display-perks-ranking="srvModel.displayPerksRanking"
|
||||
:perks-value="srvModel.perksValue"
|
||||
:active="active"
|
||||
:loading="loading"
|
||||
:in-modal="false"
|
||||
:perks="perks">
|
||||
</contribute>
|
||||
</div>
|
||||
</div>
|
||||
</b-tab>
|
||||
<b-tab title="Discussion">
|
||||
<div id="disqus_thread" class="mt-3"></div>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-8 col-sm-12" id="crowdfund-body-description-container">
|
||||
<div class="card-text overflow-hidden" v-html="srvModel.description" id="crowdfund-body-description">
|
||||
@Safe.Raw(Model.Description)
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12" id="crowdfund-body-contribution-container">
|
||||
<contribute :target-currency="srvModel.targetCurrency"
|
||||
:loading="loading"
|
||||
:display-perks-ranking="srvModel.displayPerksRanking"
|
||||
:perks-value="srvModel.perksValue"
|
||||
:active="active"
|
||||
:in-modal="false"
|
||||
:perks="perks">
|
||||
</contribute>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<noscript>
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<div class="card-text overflow-hidden">@Safe.Raw(Model.Description)</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<partial
|
||||
name="Crowdfund/Public/ContributeForm"
|
||||
model="@(new ContributeToCrowdfund { ViewCrowdfundViewModel = Model, RedirectToCheckout = true })">
|
||||
</partial>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
}
|
||||
</div>
|
||||
<div class="card-footer text-muted d-flex flex-wrap align-items-center">
|
||||
<div class="me-3" v-text="`Updated ${lastUpdated}`">Updated @Model.Info.LastUpdated</div>
|
||||
<div class="form-check me-3 my-0 only-for-js" v-if="srvModel.animationsEnabled || animation">
|
||||
<input class="form-check-input" type="checkbox" id="cbAnime" v-model="animation">
|
||||
<label class="form-check-label" for="cbAnime">Animations</label>
|
||||
|
||||
<div class="text-center mb-4" id="crowdfund-body-header">
|
||||
<button v-if="active" id="crowdfund-body-header-cta" class="btn btn-lg btn-primary py-2 px-5 only-for-js" v-on:click="contributeModalOpen = true">Contribute</button>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4 justify-content-between gap-5">
|
||||
<div class="col-lg-7 col-sm-12" id="crowdfund-body-description-container">
|
||||
<template v-if="srvModel.disqusEnabled && srvModel.disqusShortname">
|
||||
<b-tabs>
|
||||
<b-tab title="Details" active>
|
||||
<div class="overflow-hidden pt-3" v-html="srvModel.description" id="crowdfund-body-description">
|
||||
@Safe.Raw(Model.Description)
|
||||
</div>
|
||||
</b-tab>
|
||||
<b-tab title="Discussion">
|
||||
<div id="disqus_thread" class="mt-4"></div>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="overflow-hidden" v-html="srvModel.description" id="crowdfund-body-description">
|
||||
@Safe.Raw(Model.Description)
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="form-check me-3 my-0 only-for-js" v-if="srvModel.soundsEnabled|| sound">
|
||||
<input class="form-check-input" type="checkbox" id="cbSounds" v-model="sound">
|
||||
<label class="form-check-label" for="cbSounds">Sounds</label>
|
||||
<div class="col-lg-4 col-sm-12" id="crowdfund-body-contribution-container">
|
||||
<contribute :target-currency="srvModel.targetCurrency"
|
||||
:loading="loading"
|
||||
:display-perks-ranking="srvModel.displayPerksRanking"
|
||||
:perks-value="srvModel.perksValue"
|
||||
:active="active"
|
||||
:in-modal="false"
|
||||
:perks="perks">
|
||||
</contribute>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<noscript>
|
||||
<div class="row justify-content-between">
|
||||
<div class="col-md-7 col-sm-12">
|
||||
<div class="overflow-hidden">@Safe.Raw(Model.Description)</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<partial
|
||||
name="Crowdfund/Public/ContributeForm"
|
||||
model="@(new ContributeToCrowdfund { ViewCrowdfundViewModel = Model, RedirectToCheckout = true })">
|
||||
</partial>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
<div class="text-center text-muted mt-4" v-text="`Updated ${lastUpdated}`">Updated @Model.Info.LastUpdated</div>
|
||||
<b-modal title="Contribute" v-model="contributeModalOpen" size="lg" ok-only="true" ok-variant="secondary" ok-title="Close" ref="modalContribute">
|
||||
<contribute v-if="contributeModalOpen"
|
||||
:target-currency="srvModel.targetCurrency"
|
||||
@ -301,7 +280,7 @@
|
||||
<template id="perks-template">
|
||||
<div class="perks-container">
|
||||
<perk v-if="!perks || perks.length === 0"
|
||||
:perk="{title: 'Donate Custom Amount', price: { type: 0, value: null}}"
|
||||
:perk="{title: 'Donate Custom Amount', price: { type: 0, value: null }}"
|
||||
:target-currency="targetCurrency"
|
||||
:active="active"
|
||||
:loading="loading"
|
||||
@ -337,15 +316,14 @@
|
||||
<input type="hidden" :value="perk.id" id="choiceKey"/>
|
||||
<img v-if="perk.image && perk.image != 'null'" class="card-img-top" :src="perk.image"/>
|
||||
<div class="card-body">
|
||||
<div class="card-title d-flex justify-content-between">
|
||||
<span class="h5">{{perk.title ? perk.title : perk.id}}</span>
|
||||
<div class="card-title d-flex justify-content-between" :class="{ 'mb-0': !perk.description }">
|
||||
<span class="h5" :class="{ 'mb-0': !perk.description }">{{perk.title ? perk.title : perk.id}}</span>
|
||||
<span class="text-muted">
|
||||
<template v-if="perk.price && perk.price.value">
|
||||
{{formatAmount(perk.price.value.noExponents(), srvModel.currencyData.divisibility)}}
|
||||
{{targetCurrency}}
|
||||
<template v-if="perk.price.type == 1">or more</template>
|
||||
</template>
|
||||
|
||||
<template v-else-if="perk.price.type === 2 && !perk.price.value">
|
||||
Free
|
||||
</template>
|
||||
@ -356,7 +334,6 @@
|
||||
</div>
|
||||
<p class="card-text overflow-hidden" v-if="perk.description" v-html="perk.description"></p>
|
||||
|
||||
|
||||
<div class="input-group" style="max-width:500px;" v-if="expanded" :id="'perk-form'+ perk.id">
|
||||
<template v-if="perk.price.type !== 0 && !(perk.price.type === 2 && !perk.price.value)">
|
||||
<input
|
||||
|
@ -64,6 +64,11 @@
|
||||
<input asp-for="Tagline" class="form-control" />
|
||||
<span asp-validation-for="Tagline" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="MainImageUrl" class="form-label"></label>
|
||||
<input asp-for="MainImageUrl" class="form-control" />
|
||||
<span asp-validation-for="MainImageUrl" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="d-flex align-items-center">
|
||||
<input asp-for="Enabled" type="checkbox" class="btcpay-toggle me-3"/>
|
||||
@ -293,11 +298,6 @@
|
||||
</h2>
|
||||
<div id="additional-custom-css" class="accordion-collapse collapse" aria-labelledby="additional-custom-css-header">
|
||||
<div class="accordion-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="MainImageUrl" class="form-label"></label>
|
||||
<input asp-for="MainImageUrl" class="form-control" />
|
||||
<span asp-validation-for="MainImageUrl" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomCSSLink" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener">
|
||||
|
@ -5,118 +5,138 @@
|
||||
var customTipPercentages = Model.CustomTipPercentages;
|
||||
var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue);
|
||||
}
|
||||
|
||||
<script id="template-cart-item" type="text/template">
|
||||
<tr data-id="{id}">
|
||||
<td class="align-middle pe-0" width="1%">{image}</td>
|
||||
<td class="align-middle pe-0 ps-2"><b>{title}</b></td>
|
||||
<td class="align-middle px-0">
|
||||
<a class="js-cart-item-remove btn btn-link" href="#"><i class="fa fa-trash text-muted"></i></a>
|
||||
</td>
|
||||
<td class="align-middle px-0">
|
||||
<div class="input-group align-items-center">
|
||||
<a class="js-cart-item-minus btn btn-link px-2" href="#"><i class="fa fa-minus-circle fa-fw text-danger"></i></a>
|
||||
<input class="js-cart-item-count form-control form-control-sm pull-left hide-number-spin text-end" type="number" step="1" name="count" placeholder="Qty" max="{inventory}" value="{count}" data-prev="{count}">
|
||||
<a class="input-group-text js-cart-item-plus btn btn-link px-2" href="#">
|
||||
<i class="fa fa-plus-circle fa-fw text-success"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle text-end">{price}</td>
|
||||
</tr>
|
||||
</script>
|
||||
|
||||
<script id="template-cart-item-image" type="text/template">
|
||||
<img class="cart-item-image" src="{image}" alt="">
|
||||
</script>
|
||||
|
||||
<script id="template-cart-custom-amount" type="text/template">
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
|
||||
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Pay what you want">
|
||||
<div class="input-group-text">
|
||||
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
|
||||
@section PageHeadContent {
|
||||
<link rel="stylesheet" href="~/cart/css/style.css" asp-append-version="true">
|
||||
<style>
|
||||
.js-cart-item-minus .fa,
|
||||
.js-cart-item-plus .fa {
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
@section PageFootContent {
|
||||
<script>var srvModel = @Safe.Json(Model);</script>
|
||||
<script src="~/cart/js/cart.js" asp-append-version="true"></script>
|
||||
<script src="~/cart/js/cart.jquery.js" asp-append-version="true"></script>
|
||||
<script id="template-cart-item" type="text/template">
|
||||
<tr data-id="{id}">
|
||||
<td class="align-middle pe-0" width="1%">{image}</td>
|
||||
<td class="align-middle pe-0 ps-2"><b>{title}</b></td>
|
||||
<td class="align-middle px-0">
|
||||
<a class="js-cart-item-remove btn btn-link" href="#"><i class="fa fa-trash text-muted"></i></a>
|
||||
</td>
|
||||
<td class="align-middle px-0">
|
||||
<div class="input-group align-items-center">
|
||||
<a class="js-cart-item-minus btn btn-link px-2" href="#"><i class="fa fa-minus-circle fa-fw text-danger"></i></a>
|
||||
<input class="js-cart-item-count form-control form-control-sm pull-left hide-number-spin text-end" type="number" step="1" name="count" placeholder="Qty" max="{inventory}" value="{count}" data-prev="{count}">
|
||||
<a class="input-group-text js-cart-item-plus btn btn-link px-2" href="#">
|
||||
<i class="fa fa-plus-circle fa-fw text-success"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</script>
|
||||
</td>
|
||||
<td class="align-middle text-end">{price}</td>
|
||||
</tr>
|
||||
</script>
|
||||
|
||||
<script id="template-cart-extra" type="text/template">
|
||||
@if(Model.ShowCustomAmount){
|
||||
<script id="template-cart-item-image" type="text/template">
|
||||
<img class="cart-item-image" src="{image}" alt="">
|
||||
</script>
|
||||
|
||||
<script id="template-cart-custom-amount" type="text/template">
|
||||
<tr>
|
||||
<th colspan="5" class="border-0 pb-0">
|
||||
<td colspan="5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
|
||||
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" value="{customAmount}" placeholder="Pay what you want">
|
||||
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
|
||||
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Pay what you want">
|
||||
<div class="input-group-text">
|
||||
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.ShowDiscount)
|
||||
{
|
||||
<tr>
|
||||
<th colspan="5" class="border-top-0">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-percent fa-fw"></i></span>
|
||||
<input class="js-cart-discount form-control" type="number" min="0" step="@Model.Step" value="{discount}" name="discount" placeholder="Discount in %">
|
||||
<a class="js-cart-discount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<script id="template-cart-tip" type="text/template">
|
||||
@if (Model.EnableTips)
|
||||
{
|
||||
<tr>
|
||||
<th colspan="5" class="border-top-0 pt-4 h5">@Model.CustomTipText</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="5" class="border-0">
|
||||
<div class="input-group mb-2">
|
||||
<span class="input-group-text"><i class="fa fa-money fa-fw"></i></span>
|
||||
<input
|
||||
class="js-cart-tip form-control form-control-lg"
|
||||
type="number"
|
||||
min="0"
|
||||
step="@Model.Step"
|
||||
value="{tip}"
|
||||
name="tip"
|
||||
placeholder="Tip in @(Model.CurrencyInfo.CurrencySymbol != null ? Model.CurrencyInfo.CurrencySymbol : Model.CurrencyCode)"
|
||||
/>
|
||||
<a class="js-cart-tip-remove btn btn-lg btn-danger" href="#"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
<div class="row mb-1">
|
||||
@if (customTipPercentages != null && customTipPercentages.Length > 0)
|
||||
{
|
||||
@for (int i = 0; i < customTipPercentages.Length; i++)
|
||||
<script id="template-cart-extra" type="text/template">
|
||||
@if (Model.ShowCustomAmount)
|
||||
{
|
||||
<tr>
|
||||
<th colspan="5" class="border-0 pb-0">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
|
||||
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" value="{customAmount}" placeholder="Pay what you want">
|
||||
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.ShowDiscount)
|
||||
{
|
||||
<tr>
|
||||
<th colspan="5" class="border-top-0">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-percent fa-fw"></i></span>
|
||||
<input class="js-cart-discount form-control" type="number" min="0" step="@Model.Step" value="{discount}" name="discount" placeholder="Discount in %">
|
||||
<a class="js-cart-discount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="template-cart-tip" type="text/template">
|
||||
@if (Model.EnableTips)
|
||||
{
|
||||
<tr>
|
||||
<th colspan="5" class="border-top-0 pt-4 h5">@Model.CustomTipText</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="5" class="border-0">
|
||||
<div class="input-group mb-2">
|
||||
<span class="input-group-text"><i class="fa fa-money fa-fw"></i></span>
|
||||
<input
|
||||
class="js-cart-tip form-control form-control-lg"
|
||||
type="number"
|
||||
min="0"
|
||||
step="@Model.Step"
|
||||
value="{tip}"
|
||||
name="tip"
|
||||
placeholder="Tip in @(Model.CurrencyInfo.CurrencySymbol != null ? Model.CurrencyInfo.CurrencySymbol : Model.CurrencyCode)"
|
||||
/>
|
||||
<a class="js-cart-tip-remove btn btn-lg btn-danger" href="#"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
<div class="row mb-1">
|
||||
@if (customTipPercentages != null && customTipPercentages.Length > 0)
|
||||
{
|
||||
var percentage = customTipPercentages[i];
|
||||
<div class="col">
|
||||
<a class="js-cart-tip-btn btn btn-lg btn-light w-100 border mb-2" href="#" data-tip="@percentage">@percentage%</a>
|
||||
</div>
|
||||
@for (int i = 0; i < customTipPercentages.Length; i++)
|
||||
{
|
||||
var percentage = customTipPercentages[i];
|
||||
<div class="col">
|
||||
<a class="js-cart-tip-btn btn btn-lg btn-light w-100 border mb-2" href="#" data-tip="@percentage">@percentage%</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="template-cart-total" type="text/template">
|
||||
<tr>
|
||||
<th colspan="1" class="pb-4 h4">Total</th>
|
||||
<th colspan="4" class="pb-4 h4 text-end">
|
||||
<span class="js-cart-total">{total}</span>
|
||||
</th>
|
||||
</tr>}
|
||||
</script>
|
||||
|
||||
<script id="template-cart-total" type="text/template">
|
||||
<tr>
|
||||
<th colspan="1" class="pb-4 h4">Total</th>
|
||||
<th colspan="4" class="pb-4 h4 text-end">
|
||||
<span class="js-cart-total">{total}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</script>
|
||||
|
||||
</tr>
|
||||
</script>
|
||||
}
|
||||
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
|
@ -2,11 +2,104 @@
|
||||
@{
|
||||
Layout = "PointOfSale/Public/_Layout";
|
||||
}
|
||||
@section PageHeadContent {
|
||||
<style>
|
||||
.public-page-wrap {
|
||||
max-width: 560px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.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="del"] svg {
|
||||
--btn-icon-size: 2.25rem;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.actions .btn {
|
||||
flex: 1 1 50%;
|
||||
}
|
||||
|
||||
@@media (max-height: 700px) {
|
||||
.store-header {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@media (max-width: 575px) {
|
||||
.public-page-wrap {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
.keypad {
|
||||
margin-left: -1px;
|
||||
margin-right: -1px;
|
||||
}
|
||||
.store-footer {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
}
|
||||
@section PageFootContent {
|
||||
<script>var srvModel = @Safe.Json(Model);</script>
|
||||
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
||||
<script src="~/light-pos/app.js" asp-append-version="true"></script>
|
||||
}
|
||||
<div class="public-page-wrap flex-column">
|
||||
<partial name="_StatusMessage" />
|
||||
<partial name="_StoreHeader" model="(string.IsNullOrEmpty(Model.Title) ? Model.StoreName : Model.Title, Model.LogoFileId)" />
|
||||
|
||||
@if (Context.Request.Query.ContainsKey("simple"))
|
||||
{
|
||||
<partial name="PointOfSale/Public/MinimalLight" model="Model" />
|
||||
|
@ -6,15 +6,6 @@
|
||||
@inject StoreRepository StoreRepository
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
|
||||
<style>
|
||||
/* This hides unwanted metadata such as url, date, etc from appearing on a printed page. */
|
||||
@@media print {
|
||||
@@page {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@{
|
||||
var store = await StoreRepository.FindStore(Model.StoreId);
|
||||
Layout = "PointOfSale/Public/_Layout";
|
||||
@ -26,6 +17,17 @@
|
||||
supported = null;
|
||||
}
|
||||
}
|
||||
@section PageHeadContent {
|
||||
<style>
|
||||
/* This hides unwanted metadata such as url, date, etc from appearing on a printed page. */
|
||||
@@media print {
|
||||
@@page {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
@if (supported is null)
|
||||
{
|
||||
|
@ -1,90 +1,64 @@
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
|
||||
<div id="app" class="l-pos-wrapper" v-cloak>
|
||||
<form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy v-on:submit="handleFormSubmit">
|
||||
<div ref="display" class="l-pos-display pb-3 px-1"><div class="text-muted">{{srvModel.currencyCode}}</div><span ref="amount" v-bind:style="{fontSize: fontSize + 'px'}">{{ payTotal }}</span></div>
|
||||
<div class="l-pos-keypad">
|
||||
<template
|
||||
v-for="(key, index) in keys"
|
||||
:key="index">
|
||||
<div v-if="key !== ''" class="btn"
|
||||
v-bind:class="{ 'btn-primary' : (isNaN(key) === false) || key === '.', 'btn-dark' : isNaN(key) && key !== '.' }"
|
||||
v-on:click="buttonClicked(key)">{{ key }}</div>
|
||||
<div v-else class="btn btn-empty"></div>
|
||||
</template>
|
||||
<form id="app" method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy v-on:submit="handleFormSubmit" class="d-flex flex-column gap-4 flex-fill" v-cloak>
|
||||
<div ref="display" class="d-flex flex-column align-items-center px-4 mb-auto">
|
||||
<div class="fw-semibold text-muted">{{srvModel.currencyCode}}</div>
|
||||
<div class="fw-bold lh-sm" ref="amount" v-bind:style="{ fontSize: `${fontSize}px` }">{{ formatCurrency(total, false) }}</div>
|
||||
<div class="text-muted text-center mt-2" v-if="calculation">{{ calculation }}</div>
|
||||
</div>
|
||||
<div id="ModeTabs" class="tab-content mb-n2">
|
||||
<div id="Mode-Discount" class="tab-pane fade px-2" :class="{ show: mode === 'discount', active: mode === 'discount' }" role="tabpanel" aria-labelledby="ModeTablist-Discount" v-if="srvModel.showDiscount">
|
||||
<div class="h4 fw-semibold text-muted text-center">
|
||||
<span class="h3 text-body me-1">{{discountPercent || 0}}%</span> discount
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-center mt-4 gap-3">
|
||||
<div class="btn btn-outline-secondary btn-lg flex-fill" v-on:click="clearTotal">Clear</div>
|
||||
<button class="btn btn-primary btn-lg flex-fill" id="pay-button" type="submit" v-bind:disabled="payButtonLoading">
|
||||
<div v-if="payButtonLoading" class="spinner-border spinner-border-sm" id="pay-button-spinner" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
Pay
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input class="form-control" type="hidden" name="amount" v-model="payTotalNumeric">
|
||||
</form>
|
||||
|
||||
@if (Model.ShowDiscount)
|
||||
{
|
||||
<div class="input-group mt-4">
|
||||
<span class="input-group-text"><i class="fa fa-percent fa-fw"></i></span>
|
||||
<input
|
||||
class="js-cart-discount form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
v-model="discountPercent"
|
||||
v-on:change="onDiscountChange"
|
||||
name="discount"
|
||||
placeholder="Discount in %"
|
||||
>
|
||||
<a
|
||||
class="js-cart-discount-remove btn btn-danger"
|
||||
href="#"
|
||||
v-on:click="removeDiscount"
|
||||
><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.EnableTips)
|
||||
{
|
||||
<p class="pt-5 h5">@Model.CustomTipText</p>
|
||||
<div class="input-group mb-2">
|
||||
<span class="input-group-text"><i class="fa fa-money fa-fw"></i></span>
|
||||
<input
|
||||
class="js-cart-tip form-control form-control-lg"
|
||||
disabled
|
||||
type="number"
|
||||
min="0"
|
||||
step="@Model.Step"
|
||||
v-model="tipTotal"
|
||||
name="tip"
|
||||
placeholder="Tip in @(Model.CurrencyInfo.CurrencySymbol ?? Model.CurrencyCode)"
|
||||
>
|
||||
<a
|
||||
class="js-cart-tip-remove btn btn-lg btn-danger"
|
||||
href="#"
|
||||
v-on:click="removeTip"
|
||||
><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-center mt-2 gap-3">
|
||||
@if (Model.CustomTipPercentages != null && Model.CustomTipPercentages.Length > 0)
|
||||
{
|
||||
@foreach (var percentage in Model.CustomTipPercentages)
|
||||
{
|
||||
<div id="Mode-Tip" class="tab-pane fade px-2" :class="{ show: mode === 'tip', active: mode === 'tip' }" role="tabpanel" aria-labelledby="ModeTablist-Tip" v-if="srvModel.enableTips">
|
||||
<div class="btcpay-pills d-flex flex-wrap align-items-center justify-content-center gap-2">
|
||||
<template v-if="srvModel.customTipPercentages">
|
||||
<button
|
||||
class="js-cart-tip-btn btn btn-lg btn-light w-100 border mb-2"
|
||||
data-tip="@percentage"
|
||||
v-on:click="tipClicked(@percentage)"
|
||||
>
|
||||
@percentage%
|
||||
type="button"
|
||||
class="btcpay-pill"
|
||||
:class="{ active: !tipPercent }"
|
||||
v-on:click.prevent="tipPercent = null">
|
||||
<template v-if="tip && tip > 0">{{formatCurrency(tip, true)}}</template>
|
||||
<template v-else>Custom</template>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
<button
|
||||
v-for="percentage in srvModel.customTipPercentages"
|
||||
type="button"
|
||||
class="btcpay-pill"
|
||||
:class="{ active: tipPercent == percentage }"
|
||||
v-on:click.prevent="tipPercentage(percentage)">
|
||||
{{ percentage }}%
|
||||
</button>
|
||||
</template>
|
||||
<div v-else class="h5 fw-semibold text-muted text-center">
|
||||
Amount<template v-if="tip">: {{formatCurrency(tip, true)}}</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div id="ModeTablist" class="nav btcpay-pills align-items-center justify-content-center mb-n2 pb-1" role="tablist" v-if="modes.length > 1">
|
||||
<template v-for="m in modes" :key="m.value">
|
||||
<input :id="`ModeTablist-${m.type}`" name="mode" :value="m.type" type="radio" role="tab" data-bs-toggle="pill" :data-bs-target="`#Mode-${m.type}`" :disabled="m.type != 'amount' && amountNumeric == 0" :aria-controls="`Mode-${m.type}`" :aria-selected="mode === m.type" :checked="mode === m.type" v-on:click="mode = m.type">
|
||||
<label :for="`ModeTablist-${m.type}`">{{ m.title }}</label>
|
||||
</template>
|
||||
</div>
|
||||
<div class="keypad">
|
||||
<button v-for="k in keys" :key="k" v-on:click.prevent="keyPressed(k)" type="button" class="btn btn-secondary btn-lg" :data-key="k">
|
||||
<template v-if="k === 'del'"><vc:icon symbol="caret-right"/></template>
|
||||
<template v-else>{{ k }}</template>
|
||||
</button>
|
||||
</div>
|
||||
<div class="actions px-4 gap-4">
|
||||
<button class="btn btn-lg btn-secondary" type="reset" v-on:click.prevent="clear">Clear</button>
|
||||
<button class="btn btn-lg btn-primary" type="submit" :disabled="payButtonLoading" id="pay-button">
|
||||
<div v-if="payButtonLoading" class="spinner-border spinner-border-sm" id="pay-button-spinner" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<template v-else>Charge</template>
|
||||
</button>
|
||||
</div>
|
||||
<input class="form-control" type="hidden" name="amount" v-model="totalNumeric">
|
||||
</form>
|
||||
|
@ -1,6 +1,4 @@
|
||||
@using BTCPayServer.Services.Apps
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using Microsoft.AspNetCore.Hosting
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using Newtonsoft.Json.Linq
|
||||
@ -40,28 +38,6 @@
|
||||
<link rel="apple-touch-icon" href="~/img/icons/icon-512x512.png">
|
||||
<link rel="apple-touch-startup-image" href="~/img/splash.png">
|
||||
<link rel="manifest" href="@(await GetDynamicManifest(ViewData["Title"]!.ToString()))">
|
||||
@if (Model.CustomCSSLink != null)
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" asp-append-version="true" />
|
||||
}
|
||||
@if (Model.ViewType == PosViewType.Cart)
|
||||
{
|
||||
<link rel="stylesheet" href="~/cart/css/style.css" asp-append-version="true">
|
||||
<script>var srvModel = @Safe.Json(Model);</script>
|
||||
<script src="~/vendor/jquery/jquery.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/bootstrap/bootstrap.bundle.min.js" asp-append-version="true"></script>
|
||||
<script src="~/cart/js/cart.js" asp-append-version="true"></script>
|
||||
<script src="~/cart/js/cart.jquery.js" asp-append-version="true"></script>
|
||||
}
|
||||
@if (Model.ViewType == PosViewType.Light)
|
||||
{
|
||||
<link href="~/light-pos/styles/main.css" asp-append-version="true" rel="stylesheet" />
|
||||
<script>var srvModel = @Safe.Json(Model);</script>
|
||||
<script src="~/vendor/jquery/jquery.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/bootstrap/bootstrap.bundle.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
||||
<script src="~/light-pos/app.js" asp-append-version="true"></script>
|
||||
}
|
||||
<style>
|
||||
.lead :last-child {
|
||||
margin-bottom: 0;
|
||||
@ -78,23 +54,12 @@
|
||||
max-width: 320px;
|
||||
margin: auto !important;
|
||||
}
|
||||
.js-cart-item-minus .fa,
|
||||
.js-cart-item-plus .fa {
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
||||
{
|
||||
@Safe.Raw($"<style>{Model.EmbeddedCSS}</style>");
|
||||
}
|
||||
@await RenderSectionAsync("PageHeadContent", false)
|
||||
</head>
|
||||
<body class="min-vh-100">
|
||||
@RenderBody()
|
||||
<partial name="LayoutFoot"/>
|
||||
@await RenderSectionAsync("PageFootContent", false)
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,40 +1,91 @@
|
||||
let app = null;
|
||||
|
||||
document.addEventListener("DOMContentLoaded",function () {
|
||||
const displayFontSize = 80;
|
||||
app = new Vue({
|
||||
const displayFontSize = 64;
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data: function () {
|
||||
data () {
|
||||
return {
|
||||
srvModel: window.srvModel,
|
||||
payTotal: '0',
|
||||
payTotalNumeric: 0,
|
||||
tipTotal: null,
|
||||
tipTotalNumeric: 0,
|
||||
mode: 'amount',
|
||||
amount: null,
|
||||
tip: null,
|
||||
tipPercent: null,
|
||||
discount: null,
|
||||
discountPercent: null,
|
||||
discountTotalNumeric: 0,
|
||||
fontSize: displayFontSize,
|
||||
defaultFontSize: displayFontSize,
|
||||
keys: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '0', 'C'],
|
||||
payButtonLoading: false,
|
||||
keys: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '0', 'del'],
|
||||
payButtonLoading: false
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
/** We need to unset state in case user clicks the browser back button */
|
||||
window.addEventListener('pagehide', this.unsetPayButtonLoading);
|
||||
},
|
||||
destroyed: function() {
|
||||
window.removeEventListener('pagehide', this.unsetPayButtonLoading);
|
||||
},
|
||||
computed: {
|
||||
Currency: function(){
|
||||
return this.srvModel.Currency.toUpperCase();
|
||||
modes () {
|
||||
const modes = [{ title: 'Amount', type: 'amount' }]
|
||||
if (this.srvModel.showDiscount) modes.push({ title: 'Discount', type: 'discount' })
|
||||
if (this.srvModel.enableTips) modes.push({ title: 'Tip', type: 'tip'})
|
||||
return modes
|
||||
},
|
||||
keypadTarget () {
|
||||
switch (this.mode) {
|
||||
case 'amount':
|
||||
return 'amount';
|
||||
case 'discount':
|
||||
return 'discountPercent';
|
||||
case 'tip':
|
||||
return 'tip';
|
||||
}
|
||||
},
|
||||
calculation () {
|
||||
if (!this.tipNumeric && !this.discountNumeric) return null
|
||||
let calc = this.formatCurrency(this.amountNumeric, true)
|
||||
if (this.discountNumeric > 0) calc += ` - ${this.formatCurrency(this.discountNumeric, true)} (${this.discountPercent}%)`
|
||||
if (this.tipNumeric > 0) calc += ` + ${this.formatCurrency(this.tipNumeric, true)}`
|
||||
if (this.tipPercent) calc += ` (${this.tipPercent}%)`
|
||||
return calc
|
||||
},
|
||||
amountNumeric () {
|
||||
const value = parseFloat(this.amount)
|
||||
return isNaN(value) ? 0.0 : value
|
||||
},
|
||||
discountPercentNumeric () {
|
||||
const value = parseFloat(this.discountPercent)
|
||||
return isNaN(value) ? 0.0 : value;
|
||||
},
|
||||
discountNumeric () {
|
||||
return this.amountNumeric && this.discountPercentNumeric
|
||||
? this.amountNumeric * (this.discountPercentNumeric / 100)
|
||||
: 0.0;
|
||||
},
|
||||
amountMinusDiscountNumeric () {
|
||||
return this.amountNumeric - this.discountNumeric;
|
||||
},
|
||||
tipNumeric () {
|
||||
if (this.tipPercent) {
|
||||
return this.amountMinusDiscountNumeric * (this.tipPercent / 100);
|
||||
} else {
|
||||
const value = parseFloat(this.tip)
|
||||
return isNaN(value) ? 0.0 : value;
|
||||
}
|
||||
},
|
||||
total () {
|
||||
return (this.amountNumeric - this.discountNumeric + this.tipNumeric);
|
||||
},
|
||||
totalNumeric () {
|
||||
return parseFloat(this.total);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
payTotal: function() {
|
||||
// This must be timeouted because the updated width is not available yet
|
||||
this.$nextTick(function(){
|
||||
discountPercent (val) {
|
||||
const value = parseFloat(val)
|
||||
if (isNaN(value)) this.discountPercent = null
|
||||
else if (value > 100) this.discountPercent = '100'
|
||||
else this.discountPercent = value.toString();
|
||||
},
|
||||
tip (val) {
|
||||
this.tipPercent = null;
|
||||
},
|
||||
total () {
|
||||
// This must be timed out because the updated width is not available yet
|
||||
this.$nextTick(function () {
|
||||
const displayWidth = this.getWidth(this.$refs.display),
|
||||
amountWidth = this.getWidth(this.$refs.amount),
|
||||
gamma = displayWidth / amountWidth || 0,
|
||||
@ -51,96 +102,81 @@ document.addEventListener("DOMContentLoaded",function () {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getWidth: function(el) {
|
||||
getWidth (el) {
|
||||
const styles = window.getComputedStyle(el),
|
||||
width = parseFloat(el.clientWidth),
|
||||
padL = parseFloat(styles.paddingLeft),
|
||||
padR = parseFloat(styles.paddingRight);
|
||||
|
||||
return width - padL - padR;
|
||||
},
|
||||
clearTotal: function() {
|
||||
this.payTotal = '0';
|
||||
this.payTotalNumeric = 0;
|
||||
this.tipTotal = null;
|
||||
this.tipTotalNumeric = 0;
|
||||
this.discountPercent = null;
|
||||
this.discountTotalNumeric = 0;
|
||||
clear () {
|
||||
this.amount = this.tip = this.discount = this.tipPercent = this.discountPercent = null;
|
||||
this.mode = 'amount';
|
||||
},
|
||||
handleFormSubmit: function() {
|
||||
handleFormSubmit () {
|
||||
this.payButtonLoading = true;
|
||||
},
|
||||
unsetPayButtonLoading: function() {
|
||||
unsetPayButtonLoading () {
|
||||
this.payButtonLoading = false;
|
||||
},
|
||||
buttonClicked: function(key) {
|
||||
let payTotal = this.payTotal;
|
||||
|
||||
if (key === 'C') {
|
||||
payTotal = payTotal.substring(0, payTotal.length - 1);
|
||||
payTotal = payTotal === '' ? '0' : payTotal;
|
||||
formatCrypto (value, withSymbol) {
|
||||
const symbol = withSymbol ? ` ${this.srvModel.currencySymbol || this.srvModel.currencyCode}` : '';
|
||||
const divisibility = this.srvModel.currencyInfo.divisibility;
|
||||
return parseFloat(value).toFixed(divisibility) + symbol;
|
||||
},
|
||||
formatCurrency (value, withSymbol) {
|
||||
const currency = this.srvModel.currencyCode;
|
||||
if (currency === 'BTC' || currency === 'SATS') return this.formatCrypto(value, withSymbol);
|
||||
const divisibility = this.srvModel.currencyInfo.divisibility;
|
||||
const locale = currency === 'USD' ? 'en-US' : navigator.language;
|
||||
const style = withSymbol ? 'currency' : 'decimal';
|
||||
const opts = { currency, style, maximumFractionDigits: divisibility, minimumFractionDigits: divisibility };
|
||||
try {
|
||||
return new Intl.NumberFormat(locale, opts).format(value);
|
||||
} catch (err) {
|
||||
return this.formatCrypto(value, withSymbol);
|
||||
}
|
||||
},
|
||||
applyKeyToValue (key, value) {
|
||||
if (!value) value = '';
|
||||
if (key === 'del') {
|
||||
value = value.substring(0, value.length - 1);
|
||||
value = value === '' ? '0' : value;
|
||||
} else if (key === '.') {
|
||||
// Only add decimal point if it doesn't exist yet
|
||||
if (payTotal.indexOf('.') === -1) {
|
||||
payTotal += key;
|
||||
if (value.indexOf('.') === -1) {
|
||||
value += key;
|
||||
}
|
||||
} else { // Is a digit
|
||||
if (payTotal === '0') {
|
||||
payTotal = '';
|
||||
if (!value || value === '0') {
|
||||
value = '';
|
||||
}
|
||||
payTotal += key;
|
||||
|
||||
value += key;
|
||||
const { divisibility } = this.srvModel.currencyInfo;
|
||||
const decimalIndex = payTotal.indexOf('.')
|
||||
if (decimalIndex !== -1 && (payTotal.length - decimalIndex - 1 > divisibility)) {
|
||||
payTotal = payTotal.replace(".", "");
|
||||
payTotal = payTotal.substr(0, payTotal.length - divisibility) + "." +
|
||||
payTotal.substr(payTotal.length - divisibility);
|
||||
const decimalIndex = value.indexOf('.')
|
||||
if (decimalIndex !== -1 && (value.length - decimalIndex - 1 > divisibility)) {
|
||||
value = value.replace('.', '');
|
||||
value = value.substr(0, value.length - divisibility) + '.' +
|
||||
value.substr(value.length - divisibility);
|
||||
}
|
||||
}
|
||||
|
||||
this.payTotal = payTotal;
|
||||
this.payTotalNumeric = parseFloat(payTotal);
|
||||
this.tipTotalNumeric = 0;
|
||||
this.tipTotal = null;
|
||||
this.discountTotalNumeric = 0;
|
||||
this.discountPercent = null;
|
||||
return value;
|
||||
},
|
||||
tipClicked: function(percentage) {
|
||||
const { divisibility } = this.srvModel.currencyInfo;
|
||||
this.payTotalNumeric -= this.tipTotalNumeric;
|
||||
this.tipTotalNumeric = parseFloat((this.payTotalNumeric * (percentage / 100)).toFixed(divisibility));
|
||||
this.payTotalNumeric = parseFloat((this.payTotalNumeric + this.tipTotalNumeric).toFixed(divisibility));
|
||||
this.payTotal = this.payTotalNumeric.toString(10);
|
||||
this.tipTotal = this.tipTotalNumeric === 0 ? null : this.tipTotalNumeric.toFixed(divisibility);
|
||||
},
|
||||
removeTip: function() {
|
||||
this.payTotalNumeric -= this.tipTotalNumeric;
|
||||
this.payTotal = this.payTotalNumeric.toString(10);
|
||||
this.tipTotalNumeric = 0;
|
||||
this.tipTotal = null;
|
||||
},
|
||||
removeDiscount: function() {
|
||||
this.payTotalNumeric += this.discountTotalNumeric;
|
||||
this.payTotal = this.payTotalNumeric.toString(10);
|
||||
this.discountTotalNumeric = 0;
|
||||
this.discountPercent = null;
|
||||
|
||||
// Remove the tips as well as it won't be the right number anymore after discount is removed
|
||||
this.removeTip();
|
||||
},
|
||||
onDiscountChange: function (e){
|
||||
// Remove tip if we are changing discount % as it won't be the right number anymore
|
||||
this.removeTip();
|
||||
|
||||
const discountPercent = parseFloat(e.target.value);
|
||||
const { divisibility } = this.srvModel.currencyInfo;
|
||||
|
||||
this.payTotalNumeric += this.discountTotalNumeric;
|
||||
this.discountTotalNumeric = parseFloat((this.payTotalNumeric * (discountPercent / 100)).toFixed(divisibility));
|
||||
this.payTotalNumeric = parseFloat((this.payTotalNumeric - this.discountTotalNumeric).toFixed(divisibility));
|
||||
this.payTotal = this.payTotalNumeric.toString(10);
|
||||
keyPressed (key) {
|
||||
this[this.keypadTarget] = this.applyKeyToValue(key, this[this.keypadTarget]);
|
||||
},
|
||||
tipPercentage (percentage) {
|
||||
this.tipPercent = this.tipPercent !== percentage
|
||||
? percentage
|
||||
: null;
|
||||
}
|
||||
},
|
||||
created () {
|
||||
/** We need to unset state in case user clicks the browser back button */
|
||||
window.addEventListener('pagehide', this.unsetPayButtonLoading);
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('pagehide', this.unsetPayButtonLoading);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,41 +0,0 @@
|
||||
[v-cloak] > * {
|
||||
display: none
|
||||
}
|
||||
|
||||
[v-cloak]::before {
|
||||
content: "loading…"
|
||||
}
|
||||
|
||||
.l-pos-wrapper {
|
||||
max-width: 450px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.l-pos-header {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.l-pos-display {
|
||||
font-size: 1.4rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.l-pos-display span {
|
||||
display: inline-block;
|
||||
height: 80px;
|
||||
line-height: 80px;
|
||||
}
|
||||
|
||||
.l-pos-keypad .btn {
|
||||
width: 32%;
|
||||
margin-right: 1%;
|
||||
margin-bottom: 1%;
|
||||
border-radius: 0;
|
||||
padding-top: 4%;
|
||||
padding-bottom: 4%;
|
||||
font-weight: bold;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 40px;
|
||||
}
|
@ -11168,16 +11168,16 @@ ul:not([class]) li {
|
||||
|
||||
/* Button */
|
||||
.btn-outline-secondary {
|
||||
color: var(--btcpay-secondary-text);
|
||||
--btcpay-btn-color: var(--btcpay-secondary-text);
|
||||
}
|
||||
|
||||
.btn-outline-secondary:hover {
|
||||
color: var(--btcpay-secondary-text-hover);
|
||||
border-color: var(--btcpay-secondary-border-hover);
|
||||
--btcpay-btn-color: var(--btcpay-secondary-text-hover);
|
||||
--btcpay-btn-border-color: var(--btcpay-secondary-border-hover);
|
||||
}
|
||||
|
||||
.btn-outline-secondary:active {
|
||||
color: var(--btcpay-secondary-text-active);
|
||||
--btcpay-btn-color: var(--btcpay-secondary-text-active);
|
||||
}
|
||||
|
||||
.btn .icon {
|
||||
@ -11695,8 +11695,9 @@ html[data-devenv]:before {
|
||||
color: var(--btcpay-secondary-text);
|
||||
opacity: .7;
|
||||
padding: 4px 5px 3px 7px;
|
||||
font-size: 10px;
|
||||
border-top-left-radius: 4px;
|
||||
font-size: var(--btcpay-font-size-xs);
|
||||
font-family: var(--btcpay-font-family-monospace);
|
||||
border-top-left-radius: var(--btcpay-border-radius);
|
||||
}
|
||||
|
||||
@media (max-width: 575px) { html[data-devenv]:before { content: 'XS'; } }
|
||||
|
@ -608,10 +608,17 @@ input:checked + .btcpay-list-select-item {
|
||||
.public-page-wrap {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
min-height: 100vh;
|
||||
margin: 0 auto;
|
||||
padding: var(--btcpay-space-l) var(--btcpay-space-m);
|
||||
}
|
||||
|
||||
/* gradually try to set better but less supported values and units */
|
||||
.min-vh-100,
|
||||
.public-page-wrap {
|
||||
min-height: -webkit-fill-available !important;
|
||||
min-height: 100dvh !important;
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.public-page-wrap {
|
||||
padding-left: 0;
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "BTCPay Server Point of Sale",
|
||||
"short_name": "BTCPay POS",
|
||||
"theme_color": "green",
|
||||
"background_color": "white",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
|
@ -1,5 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.7.9</Version>
|
||||
<Version>1.7.10</Version>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 1.7.10
|
||||
|
||||
### Bug fix
|
||||
|
||||
* After successful migration from SQLite or MySql, there is an error after a restart @NicolasDorier
|
||||
|
||||
## 1.7.9
|
||||
|
||||
### Bug fixes
|
||||
|
Reference in New Issue
Block a user