Переглянути джерело

Add some validation to contribution form

wip/alt-interface
Jared 1 рік тому
джерело
коміт
fcb6be7d86
5 змінених файлів з 180 додано та 33 видалено
  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 Переглянути файл

@@ -6,11 +6,6 @@
Beignet
</span>
</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 class="navbar-end">
@@ -117,6 +112,9 @@ footer
background-size 28px
margin-left 4px

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

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


+ 26
- 0
src/api/types.ts Переглянути файл

@@ -43,6 +43,23 @@ export interface RewardFund {
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 {
success: boolean
}
@@ -71,6 +88,7 @@ export interface FundInfo {
title: string;
description: string;
bonuses: Bonus[];
queueID: number | null;
}

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

export interface GetBalanceRequest {
secretKey: string;
}

export interface GetBalanceResponse {
balance: number;
}

export interface ContributeRequest {
privateKey: string
amount: number


BIN
src/assets/icons8-blockchain-new-logo-24.png Переглянути файл

Перед Після
Ширина: 24  |  Висота: 24  |  Розмір: 397 B

+ 65
- 13
src/views/AddFundView.vue Переглянути файл

@@ -20,10 +20,9 @@
<input class="input is-normal has-background-white has-text-black" type="text"
placeholder="Fund Wallet" aria-label="Fund Wallet" v-model="fundWallet">
</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">
<button class="button is-success ml-1" @click="copyFundToSelling">Copy Fund</button>
</div>
<div class="control my-2">
<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"
placeholder="Memo" aria-label="Memo" v-model="memo">
<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 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 class="section px-0 py-4">
<div class="title is-5 has-text-white-ter">Bonus Structure</div>
<FundTierInput @save="saveBonuses" />
</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>
<div class="buttons is-flex is-justify-content-end mt-5">
<button
@@ -66,7 +88,11 @@
import SignetRequestController from '@/api/requests';
import {
Bonus,
CreateQueueRequest,
CreateQueueResponse,
FundInfo,
GetQueuesResponse,
Queue,
SuccessResponse,
} from '@/api/types';
import { ref } from 'vue';
@@ -91,17 +117,46 @@ const asset = ref('');
const memo = ref('');
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 saveBonuses = (evt: Bonus[]) => {
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 submit = async () => {
if (!minContribution.value) return;
if (!/^[0-9]+$/.test(minContribution.value.toString())) return;
if (!requesting.value) {
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', {
asset: asset.value,
fundWallet: sanitize(fundWallet.value),
@@ -112,6 +167,7 @@ const submit = async () => {
title: sanitize(title.value),
description: sanitize(description.value),
bonuses: bonuses.value,
queueID: forQueue.value,
});
requesting.value = false;

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

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

<style scoped lang="stylus">


+ 86
- 15
src/views/FundView.vue Переглянути файл

@@ -10,8 +10,7 @@
is-flex-desktop
is-flex-direction-row
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
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">
Contribute
</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">
<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 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 class="is-flex is-justify-content-end">
<button
class="button is-primary"
:class="requesting ? 'is-loading' : ''"
:disabled="amt > amountAvailable"
:class="loading.contribution ? 'is-loading' : ''"
:disabled="invalidContributionForm"
@click="makeContribution"
>Submit</button>
</div>
@@ -189,6 +219,8 @@ import {
ContributeRequest,
Contribution,
FundInfo,
GetBalanceRequest,
GetBalanceResponse,
GetContributionsRequest,
GetContributionsResponse,
GetRewardFundRequest,
@@ -197,6 +229,7 @@ import {
SuccessResponse,
} from '@/api/types';
import {
computed,
Ref,
ref,
watch,
@@ -296,8 +329,25 @@ const amountAvailable = ref(fund.value.fundInfo.amountAvailable);
const contributions: Ref<Contribution[]> = ref(fund.value.contributions.list ?? []);
const offset = ref(contributions.value.length);
const total = ref(fund.value.contributions.total);
const requesting = 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 = () => {
if (!fund.value) throw new Error('Fund not found!');
@@ -324,6 +374,8 @@ const calculateReward = (bought: number) => {
return reward.value.toLocaleString();
};

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

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

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 () => {
if (!fund.value) throw new Error('Fund not found');
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', {
privateKey: sanitize(pk.value),
amount: amt.value,
rewardFund: fund.value.fundInfo.id,
});
requesting.value = false;
loading.value.contribution = false;
pk.value = '';
amt.value = undefined;
}


Завантаження…
Відмінити
Зберегти