@@ -19,8 +19,8 @@ | |||
</RouterLink> | |||
</div> | |||
<div v-else> | |||
<RouterLink to="/addfund" class="button is-primary"> | |||
Add Fund | |||
<RouterLink to="/admin/dashboard" class="button is-primary"> | |||
Admin | |||
</RouterLink> | |||
<RouterLink to="/register" class="button is-white"> | |||
Register | |||
@@ -7,6 +7,7 @@ import { | |||
CreateQueueRequest, | |||
CreateQueueResponse, | |||
CreateRewardFundRequest, | |||
EditQueueRequest, | |||
GetBalanceRequest, | |||
GetBalanceResponse, | |||
GetContributionsRequest, | |||
@@ -19,6 +20,9 @@ import { | |||
GetRewardFundsRequest, | |||
GetRewardFundsResponse, | |||
LoginResponse, | |||
NearlyCompleteFundsRequest, | |||
NearlyCompleteFundsResponse, | |||
QueueMember, | |||
SuccessResponse, | |||
} from '@/api/types'; | |||
@@ -49,8 +53,7 @@ export const createRewardFund = ( | |||
issuerWallet: string, | |||
memo: string, | |||
minContribution: number, | |||
title: string, | |||
description: string, | |||
telegramLink: string, | |||
bonuses: Bonus[], | |||
queueID?: number | null | undefined, | |||
) => controller.post<SuccessResponse, CreateRewardFundRequest>('CreateRewardFund', { | |||
@@ -60,8 +63,7 @@ export const createRewardFund = ( | |||
issuerWallet, | |||
memo, | |||
minContribution, | |||
title, | |||
description, | |||
telegramLink, | |||
bonuses, | |||
queueID, | |||
}); | |||
@@ -103,3 +105,10 @@ export const contribute = (privateKey: string, amount: number, rewardFund: numbe | |||
amount, | |||
rewardFund, | |||
}); | |||
export const getNearlyCompletedFunds = (threshold: number) => controller.post<NearlyCompleteFundsResponse, NearlyCompleteFundsRequest>('NearlyCompleteFunds', { threshold }); | |||
export const reorderQueue = (queueID: number, fundOrders: QueueMember[]) => controller.post<SuccessResponse, EditQueueRequest>('EditQueue', { | |||
queueID, | |||
fundOrders, | |||
}); |
@@ -28,8 +28,7 @@ export interface RewardFund { | |||
amountGoal: number; | |||
minContribution: number; | |||
contributions: Contribution[] | null; | |||
title: string; | |||
description: string; | |||
telegramLink: string; | |||
} | |||
export interface Queue { | |||
@@ -37,6 +36,12 @@ export interface Queue { | |||
name: string; | |||
} | |||
export interface QueueMember { | |||
id?: number; | |||
asset: string; | |||
order: number; | |||
} | |||
export interface CreateQueueRequest { | |||
name: string; | |||
} | |||
@@ -70,8 +75,7 @@ export interface CreateRewardFundRequest { | |||
issuerWallet: string; | |||
memo: string; | |||
minContribution: number; | |||
title: string; | |||
description: string; | |||
telegramLink: string; | |||
queueID?: number | null; | |||
bonuses: Bonus[]; | |||
} | |||
@@ -87,8 +91,7 @@ export interface FundInfo { | |||
amountAvailable: number; | |||
amountGoal: number; | |||
minContribution: number; | |||
title: string; | |||
description: string; | |||
telegramLink: string; | |||
bonuses: Bonus[]; | |||
queueID: number | null; | |||
} | |||
@@ -149,6 +152,24 @@ export interface Claims { | |||
exp: number; | |||
} | |||
export interface NearlyCompletedFund { | |||
id: number; | |||
asset: string; | |||
minContribution: number; | |||
amountAvailable: number; | |||
memo: string; | |||
fundWallet: string; | |||
raised: number; | |||
} | |||
export interface NearlyCompleteFundsRequest { | |||
threshold: number; | |||
} | |||
export interface NearlyCompleteFundsResponse { | |||
funds: NearlyCompletedFund[]; | |||
} | |||
export interface GetContributionsRequest { | |||
id: number; | |||
offset: number; | |||
@@ -162,3 +183,8 @@ export interface CloseRewardFundRequest { | |||
id: number; | |||
close: boolean; | |||
} | |||
export interface EditQueueRequest { | |||
queueID: number; | |||
fundOrders: QueueMember[]; | |||
} |
@@ -1,7 +1,18 @@ | |||
<template> | |||
<div class="is-flex is-flex-direction-row is-justify-content-space-between"> | |||
<div class="select mr-1"> | |||
<div | |||
class="is-flex is-flex-direction-row is-justify-content-space-between" | |||
:class="props.orientation === 'horizontal' | |||
? 'is-flex-direction-row' | |||
: 'is-flex-direction-column'" | |||
> | |||
<div class="select" | |||
:class="props.orientation === 'horizontal' ? 'mr-1' : 'mr-1 mb-2'" | |||
:style="props.orientation === 'vertical' | |||
? 'width: 28%; min-width: 180px; align-self: end' | |||
: undefined" | |||
> | |||
<select | |||
style="width: 100%" | |||
ref="queueOptions" | |||
v-model="queueSelection" | |||
aria-label="Queue Selection" | |||
@@ -14,6 +25,12 @@ | |||
</option> | |||
</select> | |||
</div> | |||
<div v-if="queueSelection === undefined" class="is-flex-grow-1"> | |||
<div class="is-size-6 has-text-centered is-italic py-4"> | |||
Select an option from the menu | |||
{{ props.orientation === 'vertical' ? 'above' : 'to the left' }} | |||
</div> | |||
</div> | |||
<div v-if="queueSelection === -1" class="is-flex is-flex-direction-row is-flex-grow-1 ml-1"> | |||
<input | |||
v-model="queueName" | |||
@@ -32,7 +49,7 @@ | |||
:key="element.order" | |||
> | |||
<div class="is-flex is-flex-direction-row is-justify-content-space-between"> | |||
<div>{{ element.title }} ({{ element.asset }})</div> | |||
<div>{{ element.asset }}</div> | |||
<div>{{ element.order }}</div> | |||
</div> | |||
</div> | |||
@@ -49,23 +66,22 @@ import { | |||
} from 'vue'; | |||
import { | |||
Queue, | |||
QueueMember, | |||
RewardFund, | |||
} from '@/api/types'; | |||
import { | |||
getQueueMembers, | |||
getQueues, | |||
reorderQueue, | |||
} from '@/api/composed'; | |||
import { VueDraggableNext as Draggable } from 'vue-draggable-next'; | |||
interface QueueMember { | |||
id?: number; | |||
title: string; | |||
asset: string; | |||
order: number; | |||
} | |||
import { useDebounceFn } from '@vueuse/core'; | |||
// eslint-disable-next-line no-undef | |||
const props = defineProps<{ newMember: RewardFund & { order: number; } }>(); | |||
const props = defineProps<{ | |||
newMember: RewardFund & { order: number; }, | |||
orientation: 'horizontal' | 'vertical' | |||
}>(); | |||
// eslint-disable-next-line no-undef | |||
const emits = defineEmits(['selected', 'created']); | |||
@@ -75,13 +91,21 @@ const queueName = ref(undefined as string | undefined); | |||
const queueMembers = ref(undefined as QueueMember[] | undefined); | |||
const serverQueues = ref(0); | |||
const reorder = () => { | |||
const sendQueueOrder = useDebounceFn(async () => { | |||
if (queueSelection.value && queueMembers.value) { | |||
await reorderQueue(queueSelection.value, queueMembers.value); | |||
} | |||
}, 2000); | |||
const reorder = async () => { | |||
if (queueMembers.value) { | |||
for (let i = 0; i < queueMembers.value.length; i += 1) { | |||
if (queueMembers.value[i]) { | |||
queueMembers.value[i].order = (i + 1); | |||
} | |||
} | |||
await sendQueueOrder(); | |||
} | |||
}; | |||
@@ -107,16 +131,14 @@ const populateQueueMembers = async (id: number) => { | |||
const resp = await getQueueMembers(id) as { members: (RewardFund & { order: number; })[] }; | |||
queueMembers.value = resp?.members.map((m) => ({ | |||
id: m.id, | |||
title: m.title, | |||
asset: m.asset, | |||
order: m.order, | |||
})); | |||
serverQueues.value = queueMembers.value.length; | |||
if (queueMembers.value && props.newMember.title && props.newMember.asset) { | |||
if (queueMembers.value && props.newMember.asset) { | |||
const newMember = { | |||
id: undefined, | |||
title: props.newMember.title, | |||
asset: props.newMember.asset, | |||
order: queueMembers.value.length + 1, | |||
}; | |||
@@ -134,10 +156,9 @@ watch(queueSelection, async (newValue) => { | |||
const addedMember = computed(() => props.newMember); | |||
watch(addedMember, (newVal) => { | |||
if (newVal.title && newVal.asset) { | |||
if (newVal.asset) { | |||
const assembleNewMember = (order: number) => ({ | |||
id: undefined, | |||
title: newVal.title, | |||
asset: newVal.asset, | |||
order, | |||
}); | |||
@@ -1,8 +1,19 @@ | |||
<template> | |||
<div class="card py-2 px-4 has-text-dark" | |||
:style="generateBackgroundStyle(`${fund.asset} ${fund.title}`)"> | |||
<div class="is-size-2-desktop is-size-2-tablet is-size-3-mobile"> | |||
{{ fund.asset }} | |||
<div class="is-flex | |||
is-flex-direction-row | |||
is-justify-content-space-between | |||
is-size-2-desktop | |||
is-size-2-tablet | |||
is-size-3-mobile" | |||
> | |||
<div> | |||
{{ fund.asset }} | |||
</div> | |||
<div v-if="props.aside"> | |||
{{ props.aside }} | |||
</div> | |||
</div> | |||
<div> | |||
<ul class="is-flex is-flex-direction-row is-justify-content-space-between"> | |||
@@ -30,15 +41,18 @@ | |||
</template> | |||
<script setup lang="ts"> | |||
import { truncateWallet } from '@/lib/helpers'; | |||
import { | |||
FundInfo, | |||
} from '@/api/types'; | |||
import { FundInfo } from '@/api/types'; | |||
import { PropType } from 'vue'; | |||
// eslint-disable-next-line no-undef | |||
const props = defineProps({ fund: Object as PropType<FundInfo> }); | |||
const props = defineProps({ | |||
fund: Object as PropType<FundInfo>, | |||
aside: Object as PropType<string | number | undefined>, | |||
}); | |||
const generateHue = (seed: string) => seed.split('').map((c) => c.charCodeAt(0)).reduce((v1, v2) => v1 + v2) % 256; | |||
const generateHue = (seed: string) => seed.split('') | |||
.map((c) => c.charCodeAt(0)) | |||
.reduce((v1, v2) => v1 + v2) % 256; | |||
const generateBackgroundStyle = (hueSeed: string) => { | |||
const hue = generateHue(hueSeed); | |||
@@ -14,6 +14,9 @@ import FundView from '@/views/FundView.vue'; | |||
import AddFundView from '@/views/AddFundView.vue'; | |||
import hasPermission from '@/lib/auth'; | |||
import SignetRequestController from '@/api/requests'; | |||
import AdminView from '@/views/AdminView.vue'; | |||
import ModifyQueueView from '@/views/ModifyQueueView.vue'; | |||
import AdminDashboardView from '@/views/AdminDashboardView.vue'; | |||
const routes: Array<RouteRecordRaw> = [ | |||
{ | |||
@@ -49,12 +52,60 @@ const routes: Array<RouteRecordRaw> = [ | |||
}, | |||
}, | |||
{ | |||
path: '/addfund', | |||
name: 'addfund', | |||
component: AddFundView, | |||
path: '/admin', | |||
name: 'admin', | |||
component: AdminView, | |||
redirect: '/admin/dashboard', | |||
children: [ | |||
{ | |||
path: 'dashboard', | |||
name: 'admindashboard', | |||
component: AdminDashboardView, | |||
meta: { | |||
requiredRights: Privileges.Admin, | |||
title: 'Admin', | |||
}, | |||
}, | |||
{ | |||
path: 'addfund', | |||
name: 'addfund', | |||
component: AddFundView, | |||
meta: { | |||
requiredRights: Privileges.Admin, | |||
title: 'Add Group Fund', | |||
}, | |||
}, | |||
{ | |||
path: 'modifyfund', | |||
name: 'modifyfund', | |||
component: AddFundView, | |||
meta: { | |||
requiredRights: Privileges.Admin, | |||
title: 'Add Group Fund', | |||
}, | |||
}, | |||
{ | |||
path: 'addqueue', | |||
name: 'addqueue', | |||
component: AddFundView, | |||
meta: { | |||
requiredRights: Privileges.Admin, | |||
title: 'Add Group Fund', | |||
}, | |||
}, | |||
{ | |||
path: 'modifyqueue', | |||
name: 'modifyqueue', | |||
component: ModifyQueueView, | |||
meta: { | |||
requiredRights: Privileges.Admin, | |||
title: 'Add Group Fund', | |||
}, | |||
}, | |||
], | |||
meta: { | |||
requiredRights: Privileges.Admin, | |||
title: 'Add Group Fund', | |||
title: 'Administrator', | |||
}, | |||
}, | |||
]; | |||
@@ -49,6 +49,7 @@ | |||
<div class="title is-5 has-text-white-ter">Queue</div> | |||
<EditQueue | |||
:new-member="constructFund()" | |||
orientation="horizontal" | |||
@created="setQueueName" | |||
@selected="setQueueSelection" | |||
/> | |||
@@ -0,0 +1,39 @@ | |||
<template> | |||
<div class="container is-max-desktop"> | |||
<section class="section is-small px-0"> | |||
<template v-if="nearlyCompletedFunds.length > 0"> | |||
<div class="title is-4 has-text-white-ter has-text-centered">Nearly Completed Funds</div> | |||
<div v-for="(fund, ind) in nearlyCompletedFunds" :key="ind"> | |||
<RouterLink :to="`/fund/${fund.id}`"> | |||
<FundLink :fund="fund" :aside="`${round(fund.raised/fund.amountAvailable*100)}%`"/> | |||
</RouterLink> | |||
</div> | |||
</template> | |||
</section> | |||
</div> | |||
</template> | |||
<script setup lang="ts"> | |||
import { ref } from 'vue'; | |||
import { getNearlyCompletedFunds } from '@/api/composed'; | |||
import FundLink from '@/components/FundLink.vue'; | |||
import { NearlyCompletedFund } from '@/api/types'; | |||
const nearlyCompletedFunds = ref([] as NearlyCompletedFund[]); | |||
const pctThreshold = 85; | |||
const req = await getNearlyCompletedFunds(pctThreshold); | |||
if (req?.funds) { | |||
nearlyCompletedFunds.value = req?.funds; | |||
} | |||
const round = (float: number, digits = 1) => { | |||
const factor = 10 ** digits; | |||
return Math.round(float * factor) / factor; | |||
}; | |||
</script> | |||
<style scoped lang="stylus"> | |||
</style> |
@@ -1,15 +1,17 @@ | |||
<template> | |||
<!-- Unused --> | |||
<div class="is-flex is-flex-direction-row"> | |||
<nav class="is-hidden-tablet-only is-hidden-mobile has-background-white-ter"> | |||
<div class="has-background-primary"> | |||
<span class="has-text-white has-text-weight-bold my-4 mx-2">Administration</span> | |||
<nav class="is-hidden-tablet-only is-hidden-mobile"> | |||
<div class="mt-6"> | |||
<div> | |||
<span class="has-text-white has-text-weight-bold my-4 mx-2">Administration</span> | |||
</div> | |||
<ul class="p-2"> | |||
<li v-for="(link, i) in links" v-bind:key="i"> | |||
<RouterLink :to="link.to" class="has-text-grey-light">{{ link.text }}</RouterLink> | |||
</li> | |||
</ul> | |||
</div> | |||
<ul class="p-2"> | |||
<li v-for="(link, i) in links" v-bind:key="i"> | |||
<RouterLink :to="link.to">{{ link.text }}</RouterLink> | |||
</li> | |||
</ul> | |||
</nav> | |||
<div class="container is-max-desktop"> | |||
<RouterView></RouterView> | |||
@@ -19,16 +21,30 @@ | |||
<script setup lang="ts"> | |||
const links = [ | |||
{ text: 'Add Fund', to: '/admin/addfund' }, | |||
{ text: 'Modify Fund', to: '' }, | |||
{ text: 'Create Tag', to: '' }, | |||
{ | |||
text: 'Dashboard', | |||
to: '/admin/dashboard', | |||
}, | |||
{ | |||
text: 'Add Fund', | |||
to: '/admin/addfund', | |||
}, | |||
{ | |||
text: 'Modify Fund', | |||
to: '/admin/modifyfund', | |||
}, | |||
{ | |||
text: 'Add/Modify Queue', | |||
to: '/admin/modifyqueue', | |||
}, | |||
]; | |||
</script> | |||
<style scoped lang="stylus"> | |||
nav | |||
min-width 134px | |||
min-height 100vh | |||
height calc(100vh - 56px) | |||
li | |||
font-variant all-petite-caps | |||
@@ -1,16 +1,19 @@ | |||
<template> | |||
<div class="container is-max-desktop pb-4"> | |||
<section class="section is-small"> | |||
<div class="title is-size-4 has-text-white-ter has-text-centered"> | |||
{{ fund.fundInfo.title }} | |||
</div> | |||
<div | |||
class="is-block-mobile | |||
is-flex-tablet-only | |||
is-flex-desktop | |||
is-flex-direction-row | |||
is-justify-content-space-between"> | |||
<div class="fund-description pr-5" v-html="fixNewlines(fund.fundInfo.description)"> | |||
<div class="my-auto"> | |||
<div class="is-size-1">{{ fund.fundInfo.asset }}</div> | |||
<div> | |||
<a :href="fund.fundInfo.telegramLink" target="_blank" class="has-text-grey-light"> | |||
{{ fund.fundInfo.telegramLink }} | |||
</a> | |||
</div> | |||
</div> | |||
<div | |||
class="fund-details is-flex is-flex-direction-row is-justify-content-end my-auto py-6"> | |||
@@ -375,7 +378,7 @@ const errs: SignetError[] = [ | |||
}, | |||
]; | |||
document.title = `Beignet - ${fund.value.fundInfo.title}`; | |||
document.title = `Beignet - ${fund.value.fundInfo.asset}`; | |||
watch(selectedDate, async (newVal) => { | |||
offset.value = 0; | |||
@@ -0,0 +1,24 @@ | |||
<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/Modify Queue | |||
</div> | |||
<section class="section px-0 py-4"> | |||
<EditQueue :new-member="undefined" orientation="vertical" @created="createQueue"/> | |||
</section> | |||
</section> | |||
</div> | |||
</template> | |||
<script setup lang="ts"> | |||
import EditQueue from '@/components/EditQueue.vue'; | |||
import { createQueue } from '@/api/composed'; | |||
// | |||
</script> | |||
<style scoped lang="stylus"> | |||
</style> |