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