@@ -2,8 +2,10 @@ | |||
<nav class="navbar has-background-grey-dark" role="navigation" aria-label="main navigation"> | |||
<div class="navbar-brand"> | |||
<RouterLink to="/" class="navbar-item"> | |||
<span class="signet-logo title is-3-desktop is-4-mobile has-text-white-ter"> | |||
Beignet | |||
<span class="signet-logo title is-3-desktop is-4-mobile"> | |||
<template v-for="(element, i) in logoElements" v-bind:key="i"> | |||
<span :style="`color: #${element.color}`">{{ element.letter }}</span> | |||
</template> | |||
</span> | |||
</RouterLink> | |||
</div> | |||
@@ -32,10 +34,13 @@ | |||
<div id="content"> | |||
<RouterView v-slot="{ Component }"> | |||
<Suspense> | |||
<Component :is="Component" /> | |||
<Component :is="Component"/> | |||
<template #fallback> | |||
<span style="font-size: 4em; color: saddlebrown">Loading</span> | |||
<div class="is-flex is-flex-direction-row is-justify-content-center" | |||
style="height: 90vh"> | |||
<span style="font-size: 1.25em; color: greenyellow; margin: auto 0">Loading...</span> | |||
</div> | |||
</template> | |||
</Suspense> | |||
</RouterView> | |||
@@ -43,7 +48,8 @@ | |||
<footer> | |||
<div> | |||
Proudly made in Michigan <div class="michigan-icon"></div> | |||
Proudly made in Michigan | |||
<div class="michigan-icon"></div> | |||
</div> | |||
</footer> | |||
</template> | |||
@@ -58,7 +64,11 @@ import { | |||
import jwtDecode from 'jwt-decode'; | |||
import { Claims } from '@/api/types'; | |||
const userData = ref({ username: '', privileges: -1, exp: -1 } as Claims); | |||
const userData = ref({ | |||
username: '', | |||
privileges: -1, | |||
exp: -1, | |||
} as Claims); | |||
const state = useSessionStorage('jwt', { token: '' }); | |||
if (state.value.token) { | |||
@@ -67,6 +77,42 @@ if (state.value.token) { | |||
} | |||
const hasToken = computed(() => !!store.getters.getToken); | |||
interface LogoElement { | |||
letter: string; | |||
color: string; | |||
} | |||
const logoElements: LogoElement[] = [ | |||
{ | |||
color: '9fe82c', | |||
letter: 'B', | |||
}, | |||
{ | |||
color: '8ee045', | |||
letter: 'e', | |||
}, | |||
{ | |||
color: '7dd95c', | |||
letter: 'i', | |||
}, | |||
{ | |||
color: '6dd373', | |||
letter: 'g', | |||
}, | |||
{ | |||
color: '5dcb8a', | |||
letter: 'n', | |||
}, | |||
{ | |||
color: '4cc4a2', | |||
letter: 'e', | |||
}, | |||
{ | |||
color: '3dbeb8', | |||
letter: 't', | |||
}, | |||
]; | |||
</script> | |||
<style lang="stylus"> | |||
@@ -6,41 +6,30 @@ export enum Privileges { | |||
Admin | |||
} | |||
export interface Tag { | |||
createdAt: string; | |||
deletedAt: string; | |||
ID: number; | |||
updatedAt: string; | |||
description: string; | |||
active: boolean; | |||
contribution: number; | |||
} | |||
export interface Contribution { | |||
createdAt: string; | |||
amount: number; | |||
rewardFundID: number; | |||
tags: Tag[]; | |||
transactionID: string; | |||
wallet: string; | |||
} | |||
interface Contributions { | |||
list: Contribution[] | |||
dates: string[] | |||
total: number | |||
list: Contribution[]; | |||
dates: string[]; | |||
total: number; | |||
} | |||
export interface RewardFund { | |||
id: number | |||
asset: string | |||
wallet: string | |||
memo: string | |||
amountGoal: number | |||
minContribution: number | |||
contributions: Contribution[] | null | |||
title: string | |||
description: string | |||
id: number; | |||
asset: string; | |||
wallet: string; | |||
memo: string; | |||
amountGoal: number; | |||
minContribution: number; | |||
contributions: Contribution[] | null; | |||
title: string; | |||
description: string; | |||
} | |||
export interface Queue { | |||
@@ -57,16 +46,16 @@ export interface CreateQueueResponse { | |||
} | |||
export interface GetQueuesResponse { | |||
queues: Queue[] | |||
queues: Queue[]; | |||
} | |||
export interface SuccessResponse { | |||
success: boolean | |||
success: boolean; | |||
} | |||
export interface GetRewardFundRequest { | |||
id: number | |||
consolidateContributions: boolean | |||
id: number; | |||
consolidateContributions: boolean; | |||
} | |||
export interface Bonus { | |||
@@ -74,6 +63,19 @@ export interface Bonus { | |||
percent?: number; | |||
} | |||
export interface CreateRewardFundRequest { | |||
asset: string; | |||
fundWallet: string; | |||
sellingWallet: string; | |||
issuerWallet: string; | |||
memo: string; | |||
minContribution: number; | |||
title: string; | |||
description: string; | |||
queueID?: number | null; | |||
bonuses: Bonus[]; | |||
} | |||
export interface FundInfo { | |||
id: number; | |||
asset: string; | |||
@@ -96,9 +98,9 @@ interface Total { | |||
} | |||
export interface GetRewardFundResponse { | |||
fundInfo: FundInfo | |||
contributions: Contributions | |||
total: Total | |||
fundInfo: FundInfo; | |||
contributions: Contributions; | |||
total: Total; | |||
} | |||
export interface GetBalanceRequest { | |||
@@ -110,9 +112,9 @@ export interface GetBalanceResponse { | |||
} | |||
export interface ContributeRequest { | |||
privateKey: string | |||
amount: number | |||
rewardFund: number | |||
privateKey: string; | |||
amount: number; | |||
rewardFund: number; | |||
} | |||
export interface AuthenticationRequest { | |||
@@ -122,7 +124,7 @@ export interface AuthenticationRequest { | |||
export interface LoginResponse { | |||
token: string | null; | |||
} // TODO: change shape of fund creation request | |||
} | |||
export interface GetQueueMembersRequest { | |||
id: number; | |||
@@ -137,21 +139,21 @@ export interface GetRewardFundsRequest { | |||
} | |||
export interface GetRewardFundsResponse { | |||
rewardFunds: FundInfo[] | |||
total: number | |||
rewardFunds: FundInfo[]; | |||
total: number; | |||
} | |||
export interface Claims { | |||
username: string | |||
username: string; | |||
privileges: Privileges; | |||
exp: number; | |||
} | |||
export interface GetContributionsRequest { | |||
id: number | |||
offset: number | |||
forDate: string | undefined | |||
consolidateContributions: boolean | |||
id: number; | |||
offset: number; | |||
forDate: string | undefined; | |||
consolidateContributions: boolean; | |||
} | |||
export type GetContributionsResponse = Contributions; | |||
@@ -0,0 +1,105 @@ | |||
<template> | |||
<div class="is-flex is-flex-direction-row is-justify-content-space-between"> | |||
<div class="select mr-1"> | |||
<select | |||
ref="queueOptions" | |||
v-model="queueSelection" | |||
aria-label="Queue Selection" | |||
@change="changedSelection" | |||
> | |||
<option :value="-2">None</option> | |||
<option :value="-1">New Queue</option> | |||
<option v-for="(queue, i) in queues" v-bind:key="i" :value="queue.id"> | |||
{{ queue.name }} | |||
</option> | |||
</select> | |||
</div> | |||
<div v-if="queueSelection === -1" class="is-flex is-flex-direction-row is-flex-grow-1 ml-1"> | |||
<input | |||
v-model="queueName" | |||
aria-label="Queue Name" | |||
class="input mr-1" | |||
placeholder="Queue Name" | |||
type="text" | |||
@blur="createdQueue" | |||
> | |||
</div> | |||
<div v-else-if="queueSelection >= 0" class="is-flex-grow-1 ml-1"> | |||
<Draggable | |||
v-model="queueMembers" | |||
group="people" | |||
item-key="id" | |||
@end="drag=false" | |||
@start="drag=true"> | |||
> | |||
<template #item="{ queue }"> | |||
{{ queue.title }} | |||
</template> | |||
<div>{{ queue.title }}</div> | |||
</Draggable> | |||
</div> | |||
</div> | |||
</template> | |||
<script lang="ts" setup> | |||
import { | |||
ref, | |||
watch, | |||
} from 'vue'; | |||
import { | |||
GetQueueMembersRequest, | |||
GetQueueMembersResponse, | |||
GetQueuesResponse, | |||
Queue, | |||
RewardFund, | |||
} from '@/api/types'; | |||
import Draggable from 'vuedraggable'; | |||
import SignetRequestController from '@/api/requests'; | |||
import store from '@/store'; | |||
const controller = new SignetRequestController(store.getters.getToken); | |||
// eslint-disable-next-line no-undef | |||
const emits = defineEmits(['selected', 'created']); | |||
const queueSelection = ref(undefined as number | undefined); | |||
const queueName = ref(undefined as string | undefined); | |||
const queueMembers = ref(undefined as RewardFund[] | undefined); | |||
const drag = ref(false); | |||
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 changedSelection = () => { | |||
emits('selected', queueSelection.value); | |||
}; | |||
const createdQueue = () => { | |||
emits('created', queueName.value); | |||
}; | |||
const populateQueueMembers = async (id: number) => { | |||
const resp = await controller.post<GetQueueMembersResponse, GetQueueMembersRequest>('GetQueueMembers', { id }); | |||
queueMembers.value = resp?.members; | |||
}; | |||
watch(queueSelection, async (newValue) => { | |||
if (newValue !== undefined && newValue >= 0) { | |||
await populateQueueMembers(newValue); | |||
} | |||
}); | |||
// TODO: send new order of queued items | |||
</script> | |||
<style lang="stylus" scoped> | |||
input::placeholder, textarea::placeholder | |||
color #7d7d7d | |||
</style> |
@@ -0,0 +1,27 @@ | |||
<template> | |||
<article class="message is-danger"> | |||
<div class="message-header"> | |||
<p>Errors</p> | |||
</div> | |||
<div class="message-body"> | |||
<ol class="ml-2"> | |||
<template v-for="(err, i) in props.errors" v-bind:key="i"> | |||
<li v-show="err.condition"> | |||
{{ err.text }} | |||
</li> | |||
</template> | |||
</ol> | |||
</div> | |||
</article> | |||
</template> | |||
<script setup lang="ts"> | |||
export interface SignetError { | |||
text: string; | |||
condition: boolean; | |||
} | |||
// eslint-disable-next-line no-undef | |||
const props = defineProps<{ 'errors': SignetError[] }>(); | |||
</script> |
@@ -1,100 +1,64 @@ | |||
<template> | |||
<div class="container is-max-desktop"> | |||
<section class="section is-small"> | |||
<div class="title is-4 has-text-white-ter has-text-centered">Add Fund</div> | |||
<section class="section px-0 py-4"> | |||
<div class="title is-5 has-text-white-ter">Post</div> | |||
<div class="control my-2"> | |||
<input class="input is-normal has-background-white has-text-black" type="text" | |||
placeholder="Title" aria-label="Title" v-model="title"> | |||
</div> | |||
<div class="control my-2"> | |||
<div class="container is-max-desktop"> | |||
<section class="section is-small"> | |||
<div class="title is-4 has-text-white-ter has-text-centered">Add Fund</div> | |||
<section class="section px-0 py-4"> | |||
<div class="title is-5 has-text-white-ter">Post</div> | |||
<div class="control my-2"> | |||
<input class="input is-normal has-background-white has-text-black" type="text" | |||
placeholder="Title" aria-label="Title" v-model="title"> | |||
</div> | |||
<div class="control my-2"> | |||
<textarea class="textarea is-normal has-background-white has-text-black" | |||
placeholder="Description" aria-label="Description" v-model="description"> | |||
</textarea> | |||
</div> | |||
</section> | |||
<section class="section px-0 py-4"> | |||
<div class="title is-5 has-text-white-ter">Wallet</div> | |||
<div class="control my-2"> | |||
<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"> | |||
<input class="input is-normal has-background-white has-text-black" type="text" | |||
placeholder="Selling Wallet" aria-label="Selling Wallet" v-model="sellWallet"> | |||
</div> | |||
<div class="control my-2"> | |||
<input class="input is-normal has-background-white has-text-black" type="text" | |||
placeholder="Issuer Wallet" aria-label="Issuer Wallet" v-model="issuerWallet"> | |||
</div> | |||
</section> | |||
<section class="section px-0 py-4"> | |||
<div class="title is-5 has-text-white-ter">Fund</div> | |||
<div class="control my-2 is-flex is-justify-content-space-between"> | |||
<input class="input is-normal mr-1 has-background-white has-text-black" type="text" | |||
placeholder="Asset Code" aria-label="Asset" v-model="asset"> | |||
<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="Min Contribution" | |||
v-model="minContribution"> | |||
</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="-2">None</option> | |||
<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" | |||
> | |||
</section> | |||
<section class="section px-0 py-4"> | |||
<div class="title is-5 has-text-white-ter">Wallet</div> | |||
<div class="control my-2"> | |||
<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"> | |||
<input class="input is-normal has-background-white has-text-black" type="text" | |||
placeholder="Selling Wallet" aria-label="Selling Wallet" v-model="sellWallet"> | |||
</div> | |||
<div class="control my-2"> | |||
<input class="input is-normal has-background-white has-text-black" type="text" | |||
placeholder="Issuer Wallet" aria-label="Issuer Wallet" v-model="issuerWallet"> | |||
</div> | |||
<div class="is-flex-grow-1 ml-1" v-else-if="queueSelection >= 0"> | |||
<Draggable | |||
v-model="queueMembers" | |||
group="people" | |||
@start="drag=true" | |||
@end="drag=false" | |||
item-key="id"> | |||
> | |||
<template #item="{ queue }"> | |||
{{ queue.title }} | |||
</template> | |||
<div>{{ queue.title }}</div> | |||
</Draggable> | |||
</section> | |||
<section class="section px-0 py-4"> | |||
<div class="title is-5 has-text-white-ter">Fund</div> | |||
<div class="control my-2 is-flex is-justify-content-space-between"> | |||
<input class="input is-normal mr-1 has-background-white has-text-black" type="text" | |||
placeholder="Asset Code" aria-label="Asset" v-model="asset"> | |||
<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="Min Contribution" | |||
v-model="minContribution"> | |||
</div> | |||
</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> | |||
<EditQueue @created="setQueueName" @selected="setQueueSelection"/> | |||
</section> | |||
</section> | |||
</section> | |||
<div class="buttons is-flex is-justify-content-end mt-5"> | |||
<button | |||
class="button is-success" | |||
:class="requesting ? 'is-loading' : ''" | |||
@click="submit" | |||
>Submit</button> | |||
<div class="buttons is-flex is-justify-content-end mt-5"> | |||
<button | |||
class="button is-success" | |||
:class="requesting ? 'is-loading' : ''" | |||
@click="submit" | |||
>Submit | |||
</button> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script setup lang="ts"> | |||
@@ -104,23 +68,15 @@ import { | |||
Bonus, | |||
CreateQueueRequest, | |||
CreateQueueResponse, | |||
FundInfo, | |||
GetQueueMembersRequest, | |||
GetQueueMembersResponse, | |||
GetQueuesResponse, | |||
Queue, | |||
RewardFund, | |||
CreateRewardFundRequest, | |||
SuccessResponse, | |||
} from '@/api/types'; | |||
import { | |||
ref, | |||
watch, | |||
} from 'vue'; | |||
import { ref } from 'vue'; | |||
import store from '@/store'; | |||
import { useRouter } from 'vue-router'; | |||
import FundTierInput from '@/components/FundTierInput.vue'; | |||
import { sanitize } from '@/lib/helpers'; | |||
import Draggable from 'vuedraggable'; | |||
import EditQueue from '@/components/EditQueue.vue'; | |||
const router = useRouter(); | |||
@@ -138,20 +94,17 @@ 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 queueMembers = ref(undefined as RewardFund[] | undefined); | |||
const drag = ref(false); | |||
const queueSelection = ref(undefined as number | undefined); | |||
// TODO: figure out why the above vars are not reactive | |||
const queues = ref([] as Queue[]); | |||
const fetchQueues = async () => { | |||
const v = await controller.post<GetQueuesResponse, null>('GetQueues', null); | |||
if (v) { | |||
queues.value = v.queues; | |||
} | |||
const setQueueName = (name: string) => { | |||
queueName.value = name; | |||
}; | |||
await fetchQueues(); | |||
const setQueueSelection = (val: number) => { | |||
queueSelection.value = val; | |||
}; | |||
const bonuses = ref([] as Bonus[]); | |||
const saveBonuses = (evt: Bonus[]) => { | |||
@@ -180,7 +133,7 @@ const submit = async () => { | |||
} else { | |||
forQueue.value = queueSelection.value; | |||
} | |||
const resp = await controller.post<SuccessResponse, Partial<FundInfo>>('CreateRewardFund', { | |||
const resp = await controller.post<SuccessResponse, CreateRewardFundRequest>('CreateRewardFund', { | |||
asset: asset.value, | |||
fundWallet: sanitize(fundWallet.value), | |||
sellingWallet: sanitize(sellWallet.value), | |||
@@ -201,12 +154,6 @@ const submit = async () => { | |||
} | |||
}; | |||
watch(queueSelection, async (newValue) => { | |||
if (newValue !== undefined && newValue >= 0) { | |||
const resp = await controller.post<GetQueueMembersResponse, GetQueueMembersRequest>('GetQueueMembers', { id: newValue }); | |||
queueMembers.value = resp?.members; | |||
} | |||
}); | |||
</script> | |||
<style scoped lang="stylus"> | |||
@@ -82,30 +82,7 @@ | |||
<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> | |||
<ErrorDisplay :errors="errs" v-if="invalidContributionForm"/> | |||
<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" @blur="queryAccount"> | |||
@@ -127,7 +104,8 @@ | |||
:class="loading.contribution ? 'is-loading' : ''" | |||
:disabled="invalidContributionForm" | |||
@click="makeContribution" | |||
>Submit</button> | |||
>Submit | |||
</button> | |||
</div> | |||
</section> | |||
<section class="section is-small" v-if="contributions.length > 0"> | |||
@@ -172,8 +150,8 @@ | |||
<td>{{ truncateWallet(contribution.wallet, 6, undefined) }}</td> | |||
<td>{{ contribution.amount }}</td> | |||
<td v-if="!enableConsolidation"> | |||
<span class="transaction-date" :title="formatDate(contribution.CreatedAt, true)"> | |||
{{ formatDate(contribution.CreatedAt, true) }} | |||
<span class="transaction-date" :title="formatDate(contribution.createdAt, true)"> | |||
{{ formatDate(contribution.createdAt, true) }} | |||
</span> | |||
</td> | |||
<td v-else> | |||
@@ -243,6 +221,7 @@ import { | |||
} from '@/lib/helpers'; | |||
import * as luxon from 'luxon'; | |||
import hasPermission from '@/lib/auth'; | |||
import ErrorDisplay, { SignetError } from '@/components/ErrorDisplay.vue'; | |||
const controller = new SignetRequestController(store.getters.getToken); | |||
@@ -253,7 +232,8 @@ const { id } = route.params; | |||
const identifier = parseInt(id as string, 10); | |||
const formatDate = (time: string, includeTime = false) => { | |||
const s = luxon.DateTime.fromISO(time).toUTC(); | |||
const s = luxon.DateTime.fromISO(time) | |||
.toUTC(); | |||
const date = s.toFormat('yyyy-LLL-dd'); | |||
return includeTime ? `${date} ${s.toFormat('HH:mm')} (UTC)` : date; | |||
}; | |||
@@ -283,11 +263,18 @@ const enableConsolidation = ref(false); | |||
const fund = ref( | |||
{ | |||
fundInfo: {} as FundInfo, | |||
contributions: { list: [], dates: [] as string[], total: 0 }, | |||
contributions: { | |||
list: [], | |||
dates: [] as string[], | |||
total: 0, | |||
}, | |||
total: { amountHeld: 0 }, | |||
} as GetRewardFundResponse | null, | |||
); | |||
const fundDetails = ref([{ title: '', val: '' }]); | |||
const fundDetails = ref([{ | |||
title: '', | |||
val: '', | |||
}]); | |||
fund.value = await controller.post<GetRewardFundResponse, GetRewardFundRequest>('GetRewardFund', { | |||
id: identifier, | |||
@@ -341,10 +328,10 @@ 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); | |||
|| amt.value! > amountAvailable.value | |||
|| amt.value! < fund.value.fundInfo.minContribution | |||
|| (acctBalance.value && amt.value! > acctBalance.value) | |||
|| unknownAcct.value); | |||
}; | |||
const invalidContributionForm = computed(() => hasInvalidValues()); | |||
@@ -376,6 +363,33 @@ const calculateReward = (bought: number) => { | |||
const fixNewlines = (s: string) => s.replace('\n', '<br/>'); | |||
const errs: SignetError[] = [ | |||
{ | |||
text: 'Amount is required', | |||
condition: amt.value === undefined, | |||
}, | |||
{ | |||
text: 'Amount must be greater than 0', | |||
condition: amt.value && amt.value === 0 && !fund.value.fundInfo.minContribution, | |||
}, | |||
{ | |||
text: 'Amount is less than the minimum contribution', | |||
condition: amt.value && amt.value < fund.value.fundInfo.minContribution, | |||
}, | |||
{ | |||
text: `Not enough ${fund.value.fundInfo.asset} for sale in ICO`, | |||
condition: amt.value && amt.value > amountAvailable.value, | |||
}, | |||
{ | |||
text: `Not enough XLM to send (${amt.value?.toLocaleString()})`, | |||
condition: amt.value && acctBalance.value && amt.value > acctBalance.value, | |||
}, | |||
{ | |||
text: 'Could not find Stellar wallet', | |||
condition: unknownAcct, | |||
}, | |||
]; | |||
document.title = `Beignet - ${fund.value.fundInfo.title}`; | |||
watch(selectedDate, async (newVal) => { | |||
@@ -427,7 +441,7 @@ const { | |||
status, | |||
data, | |||
} = useWebSocket( | |||
'ws://127.0.0.1:7300/ContributorStream', // TODO: change url | |||
'ws://127.0.0.1:7300/ContributorStream', | |||
{ | |||
immediate: true, | |||
autoReconnect: true, | |||
@@ -497,13 +511,15 @@ watch(data, (newVal) => { | |||
getCurrentBonus(); | |||
if (status.value === 'OPEN') { | |||
const v = JSON.parse(newVal.trim()) as Contribution; | |||
v.CreatedAt = luxon.DateTime.now().toISO(); | |||
const formattedDate = formatDate(v.CreatedAt); | |||
v.createdAt = luxon.DateTime.now() | |||
.toISO(); | |||
const formattedDate = formatDate(v.createdAt); | |||
if (!selectableDates.value.includes(formattedDate)) { | |||
selectableDates.value.push(formattedDate); | |||
} | |||
if (enableConsolidation.value && contributions.value | |||
&& contributions.value.map((c: Contribution) => c.wallet).includes(v.wallet)) { | |||
&& contributions.value.map((c: Contribution) => c.wallet) | |||
.includes(v.wallet)) { | |||
const hasContribution = contributions.value.find((c: Contribution) => c.wallet === v.wallet); | |||
if (!hasContribution) throw new Error('Something went wrong'); | |||
hasContribution.amount += v.amount; | |||