@@ -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") | ||||
@@ -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 | ||||
@@ -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"> | ||||
@@ -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; | ||||
} | } | ||||