Browse Source

Add some validation to contribution form

wip/alt-interface
Jared 1 year ago
parent
commit
fcb6be7d86
5 changed files with 180 additions and 33 deletions
  1. +3
    -5
      src/App.vue
  2. +26
    -0
      src/api/types.ts
  3. BIN
      src/assets/icons8-blockchain-new-logo-24.png
  4. +65
    -13
      src/views/AddFundView.vue
  5. +86
    -15
      src/views/FundView.vue

+ 3
- 5
src/App.vue View File

@@ -6,11 +6,6 @@
Beignet Beignet
</span> </span>
</RouterLink> </RouterLink>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div> </div>


<div class="navbar-end"> <div class="navbar-end">
@@ -117,6 +112,9 @@ footer
background-size 28px background-size 28px
margin-left 4px margin-left 4px


.navbar-burger > *
background-color #fff !important

@font-face @font-face
font-family Paytone font-family Paytone
src url("./assets/PaytoneOne-Regular.ttf") src url("./assets/PaytoneOne-Regular.ttf")


+ 26
- 0
src/api/types.ts View File

@@ -43,6 +43,23 @@ export interface RewardFund {
description: string description: string
} }


export interface Queue {
id: number;
name: string;
}

export interface CreateQueueRequest {
name: string;
}

export interface CreateQueueResponse {
id: number;
}

export interface GetQueuesResponse {
queues: Queue[]
}

export interface SuccessResponse { export interface SuccessResponse {
success: boolean success: boolean
} }
@@ -71,6 +88,7 @@ export interface FundInfo {
title: string; title: string;
description: string; description: string;
bonuses: Bonus[]; bonuses: Bonus[];
queueID: number | null;
} }


interface Total { interface Total {
@@ -83,6 +101,14 @@ export interface GetRewardFundResponse {
total: Total total: Total
} }


export interface GetBalanceRequest {
secretKey: string;
}

export interface GetBalanceResponse {
balance: number;
}

export interface ContributeRequest { export interface ContributeRequest {
privateKey: string privateKey: string
amount: number amount: number


BIN
src/assets/icons8-blockchain-new-logo-24.png View File

Before After
Width: 24  |  Height: 24  |  Size: 397 B

+ 65
- 13
src/views/AddFundView.vue View File

@@ -20,10 +20,9 @@
<input class="input is-normal has-background-white has-text-black" type="text" <input class="input is-normal has-background-white has-text-black" type="text"
placeholder="Fund Wallet" aria-label="Fund Wallet" v-model="fundWallet"> placeholder="Fund Wallet" aria-label="Fund Wallet" v-model="fundWallet">
</div> </div>
<div class="control my-2 is-flex is-flex-direction-row">
<input class="input is-normal has-background-white has-text-black mr-1" type="text"
<div class="control my-2">
<input class="input is-normal has-background-white has-text-black" type="text"
placeholder="Selling Wallet" aria-label="Selling Wallet" v-model="sellWallet"> placeholder="Selling Wallet" aria-label="Selling Wallet" v-model="sellWallet">
<button class="button is-success ml-1" @click="copyFundToSelling">Copy Fund</button>
</div> </div>
<div class="control my-2"> <div class="control my-2">
<input class="input is-normal has-background-white has-text-black" type="text" <input class="input is-normal has-background-white has-text-black" type="text"
@@ -38,18 +37,41 @@
<input class="input is-normal mx-1 has-background-white has-text-black" type="text" <input class="input is-normal mx-1 has-background-white has-text-black" type="text"
placeholder="Memo" aria-label="Memo" v-model="memo"> placeholder="Memo" aria-label="Memo" v-model="memo">
<input class="input is-normal ml-1 has-background-white has-text-black" type="number" <input class="input is-normal ml-1 has-background-white has-text-black" type="number"
placeholder="Min Contribution" aria-label="Asset" v-model="minContribution">
placeholder="Min Contribution" aria-label="Min Contribution"
v-model="minContribution">
</div> </div>
<!-- <div class="control my-2 is-flex is-justify-content-space-between">-->

<!-- <input class="input is-normal ml-1 has-background-white has-text-black" type="number"-->
<!-- placeholder="Amount Goal" aria-label="Memo" v-model="amtGoal">-->
<!-- </div>-->
</section> </section>
<section class="section px-0 py-4"> <section class="section px-0 py-4">
<div class="title is-5 has-text-white-ter">Bonus Structure</div> <div class="title is-5 has-text-white-ter">Bonus Structure</div>
<FundTierInput @save="saveBonuses" /> <FundTierInput @save="saveBonuses" />
</section> </section>
<section class="section px-0 py-4">
<div class="title is-5 has-text-white-ter">Queue</div>
<div class="is-flex is-flex-direction-row is-justify-content-space-between">
<div class="select mr-1">
<select
v-model="queueSelection"
aria-label="Queue Selection"
ref="queueOptions"
>
<option :value="-1">New Queue</option>
<option :value="queue.id" v-for="(queue, i) in queues" v-bind:key="i">
{{ queue.name }}
</option>
</select>
</div>
<div class="is-flex is-flex-direction-row is-flex-grow-1 ml-1"
v-if="queueSelection === -1">
<input
class="input mr-1"
type="text"
placeholder="Queue Name"
v-model="queueName"
aria-label="Queue Name"
>
</div>
</div>
</section>
</section> </section>
<div class="buttons is-flex is-justify-content-end mt-5"> <div class="buttons is-flex is-justify-content-end mt-5">
<button <button
@@ -66,7 +88,11 @@
import SignetRequestController from '@/api/requests'; import SignetRequestController from '@/api/requests';
import { import {
Bonus, Bonus,
CreateQueueRequest,
CreateQueueResponse,
FundInfo, FundInfo,
GetQueuesResponse,
Queue,
SuccessResponse, SuccessResponse,
} from '@/api/types'; } from '@/api/types';
import { ref } from 'vue'; import { ref } from 'vue';
@@ -91,17 +117,46 @@ const asset = ref('');
const memo = ref(''); const memo = ref('');
const minContribution = ref(undefined as number | undefined); const minContribution = ref(undefined as number | undefined);


const queueSelection = ref(undefined as number | undefined);
const queueName = ref(undefined as string | undefined);

const queues = ref([] as Queue[]);
const fetchQueues = async () => {
const v = await controller.post<GetQueuesResponse, null>('GetQueues', null);
if (v) {
queues.value = v.queues;
}
};

await fetchQueues();

const bonuses = ref([] as Bonus[]); const bonuses = ref([] as Bonus[]);
const saveBonuses = (evt: Bonus[]) => { const saveBonuses = (evt: Bonus[]) => {
bonuses.value = evt; bonuses.value = evt;
}; };


const createQueue = async () => {
if (!queueName.value) return null;
const resp = ref(undefined as CreateQueueResponse | undefined);
resp.value = await controller.post<CreateQueueResponse, CreateQueueRequest>('CreateQueue', { name: queueName.value }) ?? undefined;
if (resp.value?.id) {
return resp.value?.id;
}
return null;
};

const requesting = ref(false); const requesting = ref(false);
const submit = async () => { const submit = async () => {
if (!minContribution.value) return; if (!minContribution.value) return;
if (!/^[0-9]+$/.test(minContribution.value.toString())) return; if (!/^[0-9]+$/.test(minContribution.value.toString())) return;
if (!requesting.value) { if (!requesting.value) {
requesting.value = true; requesting.value = true;
const forQueue = ref(undefined as null | number | undefined);
if (queueSelection.value === -1) {
forQueue.value = await createQueue();
} else {
forQueue.value = queueSelection.value;
}
const resp = await controller.post<SuccessResponse, Partial<FundInfo>>('CreateRewardFund', { const resp = await controller.post<SuccessResponse, Partial<FundInfo>>('CreateRewardFund', {
asset: asset.value, asset: asset.value,
fundWallet: sanitize(fundWallet.value), fundWallet: sanitize(fundWallet.value),
@@ -112,6 +167,7 @@ const submit = async () => {
title: sanitize(title.value), title: sanitize(title.value),
description: sanitize(description.value), description: sanitize(description.value),
bonuses: bonuses.value, bonuses: bonuses.value,
queueID: forQueue.value,
}); });
requesting.value = false; requesting.value = false;


@@ -121,10 +177,6 @@ const submit = async () => {
} }
} }
}; };

const copyFundToSelling = () => {
sellWallet.value = fundWallet.value;
};
</script> </script>


<style scoped lang="stylus"> <style scoped lang="stylus">


+ 86
- 15
src/views/FundView.vue View File

@@ -10,8 +10,7 @@
is-flex-desktop is-flex-desktop
is-flex-direction-row is-flex-direction-row
is-justify-content-space-between"> is-justify-content-space-between">
<div class="fund-description pr-5">
{{ fund.fundInfo.description }}
<div class="fund-description pr-5" v-html="fixNewlines(fund.fundInfo.description)">
</div> </div>
<div <div
class="fund-details is-flex is-flex-direction-row is-justify-content-end my-auto py-6"> class="fund-details is-flex is-flex-direction-row is-justify-content-end my-auto py-6">
@@ -83,19 +82,50 @@
<div class="title is-size-4 has-text-white-ter has-text-centered"> <div class="title is-size-4 has-text-white-ter has-text-centered">
Contribute Contribute
</div> </div>
<article class="message is-danger" v-if="invalidContributionForm">
<div class="message-header">
<p>Errors</p>
</div>
<div class="message-body">
<ol class="ml-2">
<li v-show="amt === 0 && !fund.fundInfo.minContribution">
Amount must be greater than 0
</li>
<li v-show="amt < fund.fundInfo.minContribution">
Amount is less than the minimum contribution
</li>
<li v-show="amt > amountAvailable ">
Not enough {{ fund.fundInfo.asset }} for sale in ICO
</li>
<li v-show="amt > acctBalance">
Not enough XLM to send ({{ amt.toLocaleString() }})
</li>
<li v-show="unknownAcct">
Could not find Stellar wallet
</li>
</ol>
</div>
</article>
<div class="control my-2"> <div class="control my-2">
<input class="input is-normal has-background-white has-text-black" type="text" <input class="input is-normal has-background-white has-text-black" type="text"
placeholder="Private Key" aria-label="Wallet" v-model="pk">
placeholder="Private Key" aria-label="Wallet" v-model="pk" @blur="queryAccount">
</div> </div>
<div class="control my-2">
<input class="input is-normal has-background-white has-text-black" type="number"
placeholder="Amount" aria-label="Amount" v-model="amt">
<div class="control my-2" :class="loading.balance ? 'is-loading' : null">
<input
class="input is-normal has-background-white has-text-black"
type="number"
placeholder="Amount"
aria-label="Amount"
v-model="amt"
:max="acctBalance"
:disabled="loading.balance"
>
</div> </div>
<div class="is-flex is-justify-content-end"> <div class="is-flex is-justify-content-end">
<button <button
class="button is-primary" class="button is-primary"
:class="requesting ? 'is-loading' : ''"
:disabled="amt > amountAvailable"
:class="loading.contribution ? 'is-loading' : ''"
:disabled="invalidContributionForm"
@click="makeContribution" @click="makeContribution"
>Submit</button> >Submit</button>
</div> </div>
@@ -189,6 +219,8 @@ import {
ContributeRequest, ContributeRequest,
Contribution, Contribution,
FundInfo, FundInfo,
GetBalanceRequest,
GetBalanceResponse,
GetContributionsRequest, GetContributionsRequest,
GetContributionsResponse, GetContributionsResponse,
GetRewardFundRequest, GetRewardFundRequest,
@@ -197,6 +229,7 @@ import {
SuccessResponse, SuccessResponse,
} from '@/api/types'; } from '@/api/types';
import { import {
computed,
Ref, Ref,
ref, ref,
watch, watch,
@@ -296,8 +329,25 @@ const amountAvailable = ref(fund.value.fundInfo.amountAvailable);
const contributions: Ref<Contribution[]> = ref(fund.value.contributions.list ?? []); const contributions: Ref<Contribution[]> = ref(fund.value.contributions.list ?? []);
const offset = ref(contributions.value.length); const offset = ref(contributions.value.length);
const total = ref(fund.value.contributions.total); const total = ref(fund.value.contributions.total);
const requesting = ref(false);
const contributionsLoading = ref(false); const contributionsLoading = ref(false);
const acctBalance = ref(undefined as number | undefined);
const unknownAcct = ref(true);
const loading = ref({
contribution: false,
balance: false,
});

const hasInvalidValues = () => {
if (!fund.value) throw new Error('Fund was not loaded!');
return [pk, amt].every((v) => v.value !== undefined && v.value !== '')
&& (amt.value === 0
|| amt.value! > amountAvailable.value
|| amt.value! < fund.value.fundInfo.minContribution
|| (acctBalance.value && amt.value! > acctBalance.value)
|| unknownAcct.value);
};

const invalidContributionForm = computed(() => hasInvalidValues());


const getCurrentBonus = () => { const getCurrentBonus = () => {
if (!fund.value) throw new Error('Fund not found!'); if (!fund.value) throw new Error('Fund not found!');
@@ -324,6 +374,8 @@ const calculateReward = (bought: number) => {
return reward.value.toLocaleString(); return reward.value.toLocaleString();
}; };


const fixNewlines = (s: string) => s.replace('\n', '<br/>');

document.title = `Beignet - ${fund.value.fundInfo.title}`; document.title = `Beignet - ${fund.value.fundInfo.title}`;


watch(selectedDate, async (newVal) => { watch(selectedDate, async (newVal) => {
@@ -382,20 +434,39 @@ const {
}, },
); );


const queryAccount = async () => {
if (pk.value && pk.value.startsWith('S')) {
loading.value.balance = true;
const resp = await controller.post<GetBalanceResponse, GetBalanceRequest>('GetBalance', { secretKey: pk.value });
if (resp === null) {
unknownAcct.value = true;
acctBalance.value = undefined;
} else {
unknownAcct.value = false;
if (resp?.balance) {
acctBalance.value = resp?.balance;
if (amt.value && amt.value > acctBalance.value) {
amt.value = acctBalance.value;
}
}
}
loading.value.balance = false;
}
};

const makeContribution = async () => { const makeContribution = async () => {
if (!fund.value) throw new Error('Fund not found'); if (!fund.value) throw new Error('Fund not found');
if (!amt.value) return; if (!amt.value) return;
if (!/[^[0-9]+$/.test(amt.value.toString())) {
return;
}
if (!requesting.value && pk.value && amt.value && amt.value <= amountAvailable.value) {
requesting.value = true;
if (!/[^[0-9]+$/.test(amt.value.toString())) return;
if (unknownAcct.value) return;
if (!loading.value.contribution && pk.value && amt.value && amt.value <= amountAvailable.value) {
loading.value.contribution = true;
await controller.post<SuccessResponse, ContributeRequest>('Contribute', { await controller.post<SuccessResponse, ContributeRequest>('Contribute', {
privateKey: sanitize(pk.value), privateKey: sanitize(pk.value),
amount: amt.value, amount: amt.value,
rewardFund: fund.value.fundInfo.id, rewardFund: fund.value.fundInfo.id,
}); });
requesting.value = false;
loading.value.contribution = false;
pk.value = ''; pk.value = '';
amt.value = undefined; amt.value = undefined;
} }


Loading…
Cancel
Save