This is the code that runs https://bandolier.learnjsthehardway.com/ for you to review. It uses the https://git.learnjsthehardway.com/learn-javascript-the-hard-way/bandolier-template to create the documentation for the project.
https://bandolier.learnjsthehardway.com/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
253 lines
7.3 KiB
253 lines
7.3 KiB
2 years ago
|
<script>
|
||
|
import { link } from 'svelte-spa-router';
|
||
|
import { fade } from 'svelte/transition';
|
||
|
import { onMount } from "svelte";
|
||
|
import api from "$/client/api.js";
|
||
|
import FormField from "$/client/components/FormField.svelte";
|
||
|
import Icon from "$/client/components/Icon.svelte";
|
||
|
import IconImage from "$/client/components/IconImage.svelte";
|
||
|
import Paypal from "$/client/components/Paypal.svelte";
|
||
|
import BTCPay from "$/client/components/BTCPay.svelte";
|
||
|
import Layout from "$/client/Layout.svelte";
|
||
|
import { fake_payments } from "$/client/config.js";
|
||
|
import { log } from "$/client/logging.js";
|
||
|
|
||
|
const quips = {
|
||
|
"0": "Awww, really? Alright then.",
|
||
|
"10": "That's a good start.",
|
||
|
"20": "Even more fair.",
|
||
|
"30": "Sweet!",
|
||
|
"40": "Yes! Love it!",
|
||
|
"50": "Booyah!",
|
||
|
"60": "Really?! Thank you!",
|
||
|
"70": "Whoa! No way!",
|
||
|
"80": "Fantastic! THANK YOU!",
|
||
|
"90": "NO WAY! YES!",
|
||
|
"100": "I love you!"
|
||
|
}
|
||
|
|
||
|
const form = {
|
||
|
amount: 10,
|
||
|
_errors: {},
|
||
|
_valid: true
|
||
|
}
|
||
|
|
||
|
// BUG: we have to duplicate this logic here because of how paypal works
|
||
|
$: form._valid = form.amount >= 0 && form.amount <= 100;
|
||
|
|
||
|
const amount_by = 10;
|
||
|
|
||
|
let paid_in_full = false;
|
||
|
let payment_failed = false;
|
||
|
|
||
|
const change_amount = () => {
|
||
|
form.amount = form.amount < 100 ? form.amount + amount_by : 0;
|
||
|
}
|
||
|
|
||
|
const payment_finished = (event) => {
|
||
|
const { system, status } = event.detail;
|
||
|
log.assert(status === "complete", "Payment status should be complete with finished event", system, event.detail);
|
||
|
paid_in_full = true;
|
||
|
}
|
||
|
|
||
|
const payment_canceled = (event) => {
|
||
|
log.debug("CANCELED", event);
|
||
|
}
|
||
|
|
||
|
const payment_error = () => {
|
||
|
payment_failed = true;
|
||
|
}
|
||
|
|
||
|
const payment_loading = (event) => {
|
||
|
log.debug("LOADING", event);
|
||
|
}
|
||
|
|
||
|
const pay_nothing = async () => {
|
||
|
// posting to /api/user/payments is how you do a free purchase
|
||
|
let [status, data] = await api.post("/api/user/payments", { amount: form.amount }, api.logout_user);
|
||
|
|
||
|
if(status === 200) {
|
||
|
paid_in_full = true;
|
||
|
} else {
|
||
|
log.error("status", status, "data", data);
|
||
|
payment_failed = true;
|
||
|
form._errors.main = data.message || data.error || "Payment Error.";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// this is a demo of how to check a user's purchase quickly
|
||
|
const already_paid = async () => {
|
||
|
let [status, data] = await api.get("/api/user/payments");
|
||
|
|
||
|
if(status === 200 && data.paid === true) {
|
||
|
paid_in_full = true;
|
||
|
} else {
|
||
|
log.debug("GET to /api/user/payments returned", status, data);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
onMount(async () => {
|
||
|
await already_paid();
|
||
|
});
|
||
|
</script>
|
||
|
|
||
|
<style>
|
||
|
|
||
|
form#purchase {
|
||
|
width: min-content;
|
||
|
}
|
||
|
|
||
|
label.slider {
|
||
|
cursor: pointer;
|
||
|
display: flex;
|
||
|
height: 4rem;
|
||
|
width: 100%;
|
||
|
background-color: var(--color-bg-secondary);
|
||
|
user-select: none;
|
||
|
-webkit-user-select: none;
|
||
|
}
|
||
|
|
||
|
label.slider::after {
|
||
|
display: flex;
|
||
|
justify-content: center;
|
||
|
align-items: center;
|
||
|
width: calc(1% * var(--amount));
|
||
|
min-width: 2ch;
|
||
|
background-color: var(--color-bg-tertiary);
|
||
|
height: 4rem;
|
||
|
counter-reset: amount var(--amount);
|
||
|
content: '$' counter(amount);
|
||
|
transition: 0.5s;
|
||
|
}
|
||
|
|
||
|
input#amount {
|
||
|
display: none;
|
||
|
}
|
||
|
|
||
|
card {
|
||
|
width: 400px;
|
||
|
border-radius: var(--border-radius) var(--border-radius) 0px 0px;
|
||
|
}
|
||
|
|
||
|
card#failed top {
|
||
|
background-color: var(--color-bg-secondary);
|
||
|
text-align: center;
|
||
|
padding: 1rem;
|
||
|
}
|
||
|
|
||
|
payments {
|
||
|
display: flex;
|
||
|
flex-direction: column;
|
||
|
}
|
||
|
|
||
|
payments.disabled {
|
||
|
filter: blur(5px);
|
||
|
}
|
||
|
|
||
|
form card bottom {
|
||
|
padding: 0.5rem;
|
||
|
}
|
||
|
|
||
|
callout {
|
||
|
border-radius: 0px 0px var(--border-radius) var(--border-radius);
|
||
|
}
|
||
|
</style>
|
||
|
|
||
|
<Layout centered={ true } authenticated={ true }>
|
||
|
{#if paid_in_full}
|
||
|
<card in:fade|local id="paid">
|
||
|
<top>
|
||
|
<IconImage name="dollar-sign" />
|
||
|
</top>
|
||
|
|
||
|
<middle>
|
||
|
<h1>Welcome!</h1>
|
||
|
<p>Thank you for your purchase. You can now enjoy the entire
|
||
|
site.
|
||
|
</p>
|
||
|
</middle>
|
||
|
|
||
|
<bottom>
|
||
|
<button-group>
|
||
|
<button data-testid="payment-done-button"><a href="/" use:link>Start Browsing</a></button>
|
||
|
</button-group>
|
||
|
</bottom>
|
||
|
</card>
|
||
|
{:else}
|
||
|
{#if payment_failed}
|
||
|
<card in:fade|local id="failed">
|
||
|
<top><h1 data-testid="payment-error">Payment Error!</h1></top>
|
||
|
|
||
|
<middle>
|
||
|
<p style="font-size: 1.5em;">There was an error processing your payment. Please try again later.</p>
|
||
|
<p>You can email <a href="mailto:help@xor.academy">help@xor.academy</a> to get help with this purchase.</p>
|
||
|
</middle>
|
||
|
|
||
|
<bottom>
|
||
|
<button-group>
|
||
|
<button type="button"><a href="/" use:link><Icon name="arrow-left-circle" size="36" /> Cancel</a></button>
|
||
|
<button data-testid="payment-tryagain" on:click={ () => payment_failed = false }><Icon name="credit-card" size="36" light={ true } /> Try Again</button>
|
||
|
</button-group>
|
||
|
</bottom>
|
||
|
</card>
|
||
|
{:else}
|
||
|
<form method="POST" id="purchase">
|
||
|
<card>
|
||
|
<top>
|
||
|
{#if payment_failed }
|
||
|
<h1>Payment Error!</h1>
|
||
|
{:else}
|
||
|
<h1>What's a Fair Price?</h1>
|
||
|
{/if}
|
||
|
</top>
|
||
|
|
||
|
<middle>
|
||
|
<FormField form={ form } field="amount" label="{ quips[form.amount] || `Bad Value! ${form.amount}` }">
|
||
|
<label data-testid="payment-amount" class="slider"
|
||
|
for="amount" style="--amount: { form.amount };"
|
||
|
on:click={ () => change_amount() }>
|
||
|
</label>
|
||
|
<input type="numeric" id="amount" bind:value={ form.amount } />
|
||
|
</FormField>
|
||
|
|
||
|
{#if form._valid}
|
||
|
<p>Click on the slider to change <b>how much you think this course is worth</b>, then
|
||
|
<b>select your payment method</b>.</p>
|
||
|
{:else}
|
||
|
<p><b>You've caused an error in this form. Please email help@xor.academy and tell them how you did this.</b></p>
|
||
|
{/if}
|
||
|
</middle>
|
||
|
|
||
|
<bottom>
|
||
|
<payments class:disabled={ !form._valid }>
|
||
|
{#if form.amount > 0}
|
||
|
<Paypal credit_card={ true } amount={ form.amount }
|
||
|
on:error={ payment_error}
|
||
|
on:finished={ payment_finished }
|
||
|
on:canceled={ payment_canceled }
|
||
|
on:loading={ payment_loading }
|
||
|
disabled = { !form._valid }/>
|
||
|
|
||
|
<BTCPay amount={ form.amount }
|
||
|
on:error={ payment_error}
|
||
|
on:finished={ payment_finished }
|
||
|
on:canceled={ payment_canceled }
|
||
|
on:loading={ payment_loading }
|
||
|
disabled = { !form._valid } />
|
||
|
{:else}
|
||
|
<button type="button" data-testid="button-paynothing" on:click|preventDefault={ pay_nothing }>Pay Nothing</button>
|
||
|
{/if}
|
||
|
</payments>
|
||
|
</bottom>
|
||
|
</card>
|
||
|
{#if fake_payments}
|
||
|
<callout class="warning">
|
||
|
Payments are fake right now. Use a fake CC from Paypal,
|
||
|
and use testnet coins for BTC/LTC.
|
||
|
</callout>
|
||
|
{/if}
|
||
|
</form>
|
||
|
{/if}
|
||
|
{/if}
|
||
|
</Layout>
|