Browse Source

Componentized the queue editor, among other things

wip/alt-interface
Jared 1 year ago
parent
commit
2599da4a1e
6 changed files with 344 additions and 201 deletions
  1. +52
    -6
      src/App.vue
  2. +43
    -41
      src/api/types.ts
  3. +105
    -0
      src/components/EditQueue.vue
  4. +27
    -0
      src/components/ErrorDisplay.vue
  5. +63
    -116
      src/views/AddFundView.vue
  6. +54
    -38
      src/views/FundView.vue

+ 52
- 6
src/App.vue View File

@@ -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">


+ 43
- 41
src/api/types.ts View File

@@ -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;


+ 105
- 0
src/components/EditQueue.vue View File

@@ -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>

+ 27
- 0
src/components/ErrorDisplay.vue View File

@@ -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>

+ 63
- 116
src/views/AddFundView.vue View File

@@ -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">


+ 54
- 38
src/views/FundView.vue View File

@@ -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;


Loading…
Cancel
Save