| @@ -2,8 +2,10 @@ | |||||
| <nav class="navbar has-background-grey-dark" role="navigation" aria-label="main navigation"> | <nav class="navbar has-background-grey-dark" role="navigation" aria-label="main navigation"> | ||||
| <div class="navbar-brand"> | <div class="navbar-brand"> | ||||
| <RouterLink to="/" class="navbar-item"> | <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> | </span> | ||||
| </RouterLink> | </RouterLink> | ||||
| </div> | </div> | ||||
| @@ -32,10 +34,13 @@ | |||||
| <div id="content"> | <div id="content"> | ||||
| <RouterView v-slot="{ Component }"> | <RouterView v-slot="{ Component }"> | ||||
| <Suspense> | <Suspense> | ||||
| <Component :is="Component" /> | |||||
| <Component :is="Component"/> | |||||
| <template #fallback> | <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> | </template> | ||||
| </Suspense> | </Suspense> | ||||
| </RouterView> | </RouterView> | ||||
| @@ -43,7 +48,8 @@ | |||||
| <footer> | <footer> | ||||
| <div> | <div> | ||||
| Proudly made in Michigan <div class="michigan-icon"></div> | |||||
| Proudly made in Michigan | |||||
| <div class="michigan-icon"></div> | |||||
| </div> | </div> | ||||
| </footer> | </footer> | ||||
| </template> | </template> | ||||
| @@ -58,7 +64,11 @@ import { | |||||
| import jwtDecode from 'jwt-decode'; | import jwtDecode from 'jwt-decode'; | ||||
| import { Claims } from '@/api/types'; | 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: '' }); | const state = useSessionStorage('jwt', { token: '' }); | ||||
| if (state.value.token) { | if (state.value.token) { | ||||
| @@ -67,6 +77,42 @@ if (state.value.token) { | |||||
| } | } | ||||
| const hasToken = computed(() => !!store.getters.getToken); | 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> | </script> | ||||
| <style lang="stylus"> | <style lang="stylus"> | ||||
| @@ -6,41 +6,30 @@ export enum Privileges { | |||||
| Admin | Admin | ||||
| } | } | ||||
| export interface Tag { | |||||
| createdAt: string; | |||||
| deletedAt: string; | |||||
| ID: number; | |||||
| updatedAt: string; | |||||
| description: string; | |||||
| active: boolean; | |||||
| contribution: number; | |||||
| } | |||||
| export interface Contribution { | export interface Contribution { | ||||
| createdAt: string; | createdAt: string; | ||||
| amount: number; | amount: number; | ||||
| rewardFundID: number; | rewardFundID: number; | ||||
| tags: Tag[]; | |||||
| transactionID: string; | transactionID: string; | ||||
| wallet: string; | wallet: string; | ||||
| } | } | ||||
| interface Contributions { | interface Contributions { | ||||
| list: Contribution[] | |||||
| dates: string[] | |||||
| total: number | |||||
| list: Contribution[]; | |||||
| dates: string[]; | |||||
| total: number; | |||||
| } | } | ||||
| export interface RewardFund { | 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 { | export interface Queue { | ||||
| @@ -57,16 +46,16 @@ export interface CreateQueueResponse { | |||||
| } | } | ||||
| export interface GetQueuesResponse { | export interface GetQueuesResponse { | ||||
| queues: Queue[] | |||||
| queues: Queue[]; | |||||
| } | } | ||||
| export interface SuccessResponse { | export interface SuccessResponse { | ||||
| success: boolean | |||||
| success: boolean; | |||||
| } | } | ||||
| export interface GetRewardFundRequest { | export interface GetRewardFundRequest { | ||||
| id: number | |||||
| consolidateContributions: boolean | |||||
| id: number; | |||||
| consolidateContributions: boolean; | |||||
| } | } | ||||
| export interface Bonus { | export interface Bonus { | ||||
| @@ -74,6 +63,19 @@ export interface Bonus { | |||||
| percent?: number; | 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 { | export interface FundInfo { | ||||
| id: number; | id: number; | ||||
| asset: string; | asset: string; | ||||
| @@ -96,9 +98,9 @@ interface Total { | |||||
| } | } | ||||
| export interface GetRewardFundResponse { | export interface GetRewardFundResponse { | ||||
| fundInfo: FundInfo | |||||
| contributions: Contributions | |||||
| total: Total | |||||
| fundInfo: FundInfo; | |||||
| contributions: Contributions; | |||||
| total: Total; | |||||
| } | } | ||||
| export interface GetBalanceRequest { | export interface GetBalanceRequest { | ||||
| @@ -110,9 +112,9 @@ export interface GetBalanceResponse { | |||||
| } | } | ||||
| export interface ContributeRequest { | export interface ContributeRequest { | ||||
| privateKey: string | |||||
| amount: number | |||||
| rewardFund: number | |||||
| privateKey: string; | |||||
| amount: number; | |||||
| rewardFund: number; | |||||
| } | } | ||||
| export interface AuthenticationRequest { | export interface AuthenticationRequest { | ||||
| @@ -122,7 +124,7 @@ export interface AuthenticationRequest { | |||||
| export interface LoginResponse { | export interface LoginResponse { | ||||
| token: string | null; | token: string | null; | ||||
| } // TODO: change shape of fund creation request | |||||
| } | |||||
| export interface GetQueueMembersRequest { | export interface GetQueueMembersRequest { | ||||
| id: number; | id: number; | ||||
| @@ -137,21 +139,21 @@ export interface GetRewardFundsRequest { | |||||
| } | } | ||||
| export interface GetRewardFundsResponse { | export interface GetRewardFundsResponse { | ||||
| rewardFunds: FundInfo[] | |||||
| total: number | |||||
| rewardFunds: FundInfo[]; | |||||
| total: number; | |||||
| } | } | ||||
| export interface Claims { | export interface Claims { | ||||
| username: string | |||||
| username: string; | |||||
| privileges: Privileges; | privileges: Privileges; | ||||
| exp: number; | exp: number; | ||||
| } | } | ||||
| export interface GetContributionsRequest { | 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; | 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> | <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" | <textarea class="textarea is-normal has-background-white has-text-black" | ||||
| placeholder="Description" aria-label="Description" v-model="description"> | placeholder="Description" aria-label="Description" v-model="description"> | ||||
| </textarea> | </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> | ||||
| <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> | ||||
| <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> | ||||
| </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> | ||||
| </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> | ||||
| </div> | |||||
| </template> | </template> | ||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||
| @@ -104,23 +68,15 @@ import { | |||||
| Bonus, | Bonus, | ||||
| CreateQueueRequest, | CreateQueueRequest, | ||||
| CreateQueueResponse, | CreateQueueResponse, | ||||
| FundInfo, | |||||
| GetQueueMembersRequest, | |||||
| GetQueueMembersResponse, | |||||
| GetQueuesResponse, | |||||
| Queue, | |||||
| RewardFund, | |||||
| CreateRewardFundRequest, | |||||
| SuccessResponse, | SuccessResponse, | ||||
| } from '@/api/types'; | } from '@/api/types'; | ||||
| import { | |||||
| ref, | |||||
| watch, | |||||
| } from 'vue'; | |||||
| import { ref } from 'vue'; | |||||
| import store from '@/store'; | import store from '@/store'; | ||||
| import { useRouter } from 'vue-router'; | import { useRouter } from 'vue-router'; | ||||
| import FundTierInput from '@/components/FundTierInput.vue'; | import FundTierInput from '@/components/FundTierInput.vue'; | ||||
| import { sanitize } from '@/lib/helpers'; | import { sanitize } from '@/lib/helpers'; | ||||
| import Draggable from 'vuedraggable'; | |||||
| import EditQueue from '@/components/EditQueue.vue'; | |||||
| const router = useRouter(); | const router = useRouter(); | ||||
| @@ -138,20 +94,17 @@ 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 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 bonuses = ref([] as Bonus[]); | ||||
| const saveBonuses = (evt: Bonus[]) => { | const saveBonuses = (evt: Bonus[]) => { | ||||
| @@ -180,7 +133,7 @@ const submit = async () => { | |||||
| } else { | } else { | ||||
| forQueue.value = queueSelection.value; | forQueue.value = queueSelection.value; | ||||
| } | } | ||||
| const resp = await controller.post<SuccessResponse, Partial<FundInfo>>('CreateRewardFund', { | |||||
| const resp = await controller.post<SuccessResponse, CreateRewardFundRequest>('CreateRewardFund', { | |||||
| asset: asset.value, | asset: asset.value, | ||||
| fundWallet: sanitize(fundWallet.value), | fundWallet: sanitize(fundWallet.value), | ||||
| sellingWallet: sanitize(sellWallet.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> | </script> | ||||
| <style scoped lang="stylus"> | <style scoped lang="stylus"> | ||||
| @@ -82,30 +82,7 @@ | |||||
| <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> | |||||
| <ErrorDisplay :errors="errs" v-if="invalidContributionForm"/> | |||||
| <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" @blur="queryAccount"> | placeholder="Private Key" aria-label="Wallet" v-model="pk" @blur="queryAccount"> | ||||
| @@ -127,7 +104,8 @@ | |||||
| :class="loading.contribution ? 'is-loading' : ''" | :class="loading.contribution ? 'is-loading' : ''" | ||||
| :disabled="invalidContributionForm" | :disabled="invalidContributionForm" | ||||
| @click="makeContribution" | @click="makeContribution" | ||||
| >Submit</button> | |||||
| >Submit | |||||
| </button> | |||||
| </div> | </div> | ||||
| </section> | </section> | ||||
| <section class="section is-small" v-if="contributions.length > 0"> | <section class="section is-small" v-if="contributions.length > 0"> | ||||
| @@ -172,8 +150,8 @@ | |||||
| <td>{{ truncateWallet(contribution.wallet, 6, undefined) }}</td> | <td>{{ truncateWallet(contribution.wallet, 6, undefined) }}</td> | ||||
| <td>{{ contribution.amount }}</td> | <td>{{ contribution.amount }}</td> | ||||
| <td v-if="!enableConsolidation"> | <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> | </span> | ||||
| </td> | </td> | ||||
| <td v-else> | <td v-else> | ||||
| @@ -243,6 +221,7 @@ import { | |||||
| } from '@/lib/helpers'; | } from '@/lib/helpers'; | ||||
| import * as luxon from 'luxon'; | import * as luxon from 'luxon'; | ||||
| import hasPermission from '@/lib/auth'; | import hasPermission from '@/lib/auth'; | ||||
| import ErrorDisplay, { SignetError } from '@/components/ErrorDisplay.vue'; | |||||
| const controller = new SignetRequestController(store.getters.getToken); | const controller = new SignetRequestController(store.getters.getToken); | ||||
| @@ -253,7 +232,8 @@ const { id } = route.params; | |||||
| const identifier = parseInt(id as string, 10); | const identifier = parseInt(id as string, 10); | ||||
| const formatDate = (time: string, includeTime = false) => { | 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'); | const date = s.toFormat('yyyy-LLL-dd'); | ||||
| return includeTime ? `${date} ${s.toFormat('HH:mm')} (UTC)` : date; | return includeTime ? `${date} ${s.toFormat('HH:mm')} (UTC)` : date; | ||||
| }; | }; | ||||
| @@ -283,11 +263,18 @@ const enableConsolidation = ref(false); | |||||
| const fund = ref( | const fund = ref( | ||||
| { | { | ||||
| fundInfo: {} as FundInfo, | fundInfo: {} as FundInfo, | ||||
| contributions: { list: [], dates: [] as string[], total: 0 }, | |||||
| contributions: { | |||||
| list: [], | |||||
| dates: [] as string[], | |||||
| total: 0, | |||||
| }, | |||||
| total: { amountHeld: 0 }, | total: { amountHeld: 0 }, | ||||
| } as GetRewardFundResponse | null, | } as GetRewardFundResponse | null, | ||||
| ); | ); | ||||
| const fundDetails = ref([{ title: '', val: '' }]); | |||||
| const fundDetails = ref([{ | |||||
| title: '', | |||||
| val: '', | |||||
| }]); | |||||
| fund.value = await controller.post<GetRewardFundResponse, GetRewardFundRequest>('GetRewardFund', { | fund.value = await controller.post<GetRewardFundResponse, GetRewardFundRequest>('GetRewardFund', { | ||||
| id: identifier, | id: identifier, | ||||
| @@ -341,10 +328,10 @@ const hasInvalidValues = () => { | |||||
| if (!fund.value) throw new Error('Fund was not loaded!'); | if (!fund.value) throw new Error('Fund was not loaded!'); | ||||
| return [pk, amt].every((v) => v.value !== undefined && v.value !== '') | return [pk, amt].every((v) => v.value !== undefined && v.value !== '') | ||||
| && (amt.value === 0 | && (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()); | const invalidContributionForm = computed(() => hasInvalidValues()); | ||||
| @@ -376,6 +363,33 @@ const calculateReward = (bought: number) => { | |||||
| const fixNewlines = (s: string) => s.replace('\n', '<br/>'); | 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}`; | document.title = `Beignet - ${fund.value.fundInfo.title}`; | ||||
| watch(selectedDate, async (newVal) => { | watch(selectedDate, async (newVal) => { | ||||
| @@ -427,7 +441,7 @@ const { | |||||
| status, | status, | ||||
| data, | data, | ||||
| } = useWebSocket( | } = useWebSocket( | ||||
| 'ws://127.0.0.1:7300/ContributorStream', // TODO: change url | |||||
| 'ws://127.0.0.1:7300/ContributorStream', | |||||
| { | { | ||||
| immediate: true, | immediate: true, | ||||
| autoReconnect: true, | autoReconnect: true, | ||||
| @@ -497,13 +511,15 @@ watch(data, (newVal) => { | |||||
| getCurrentBonus(); | getCurrentBonus(); | ||||
| if (status.value === 'OPEN') { | if (status.value === 'OPEN') { | ||||
| const v = JSON.parse(newVal.trim()) as Contribution; | 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)) { | if (!selectableDates.value.includes(formattedDate)) { | ||||
| selectableDates.value.push(formattedDate); | selectableDates.value.push(formattedDate); | ||||
| } | } | ||||
| if (enableConsolidation.value && contributions.value | 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); | const hasContribution = contributions.value.find((c: Contribution) => c.wallet === v.wallet); | ||||
| if (!hasContribution) throw new Error('Something went wrong'); | if (!hasContribution) throw new Error('Something went wrong'); | ||||
| hasContribution.amount += v.amount; | hasContribution.amount += v.amount; | ||||