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