@@ -43,6 +43,12 @@ | |||||
</Suspense> | </Suspense> | ||||
</RouterView> | </RouterView> | ||||
</div> | </div> | ||||
<footer> | |||||
<div> | |||||
Proudly made in Michigan <div class="michigan-icon"></div> | |||||
</div> | |||||
</footer> | |||||
</template> | </template> | ||||
<script setup lang="ts"> | <script setup lang="ts"> | ||||
@@ -85,4 +91,23 @@ body | |||||
min-height 100vh | min-height 100vh | ||||
color #e8dbca | color #e8dbca | ||||
background-color #313538 | background-color #313538 | ||||
footer | |||||
color #707070 | |||||
padding 12px | |||||
margin-top 10px | |||||
& * | |||||
vertical-align middle | |||||
text-align center | |||||
.michigan-icon | |||||
display inline-block | |||||
width 28px | |||||
height 28px | |||||
background-image url("./assets/icons8-michigan-50.png") | |||||
background-repeat no-repeat | |||||
background-position center | |||||
background-size 28px | |||||
margin-left 4px | |||||
</style> | </style> |
@@ -61,10 +61,11 @@ export interface FundInfo { | |||||
id: number; | id: number; | ||||
asset: string; | asset: string; | ||||
fundWallet: string; | fundWallet: string; | ||||
fundSecret: string; | |||||
sellingWallet: string; | |||||
issuerWallet: string; | issuerWallet: string; | ||||
memo: string; | memo: string; | ||||
price: number; | price: number; | ||||
amountAvailable: number; | |||||
amountGoal: number; | amountGoal: number; | ||||
minContribution: number; | minContribution: number; | ||||
title: string; | title: string; | ||||
@@ -12,7 +12,7 @@ | |||||
</li> | </li> | ||||
<li class="is-size-7 is-inline-block px-2"> | <li class="is-size-7 is-inline-block px-2"> | ||||
<span class="stellar-icon-base goal"></span> | <span class="stellar-icon-base goal"></span> | ||||
<span class="fund-label">{{ props.fund.amountGoal.toLocaleString() }}</span> | |||||
<span class="fund-label">{{ props.fund.amountAvailable.toLocaleString() }}</span> | |||||
</li> | </li> | ||||
<li class="is-size-7 is-inline-block px-2"> | <li class="is-size-7 is-inline-block px-2"> | ||||
<span class="stellar-icon-base memo"></span> | <span class="stellar-icon-base memo"></span> | ||||
@@ -1,6 +1,2 @@ | |||||
import { useRouter } from 'vue-router'; | |||||
const router = useRouter(); | |||||
export const truncateWallet: (wallet: string, preDigits: number, postDigits: number | undefined) => string = (wallet: string, preDigits: number, postDigits = preDigits) => `${wallet.slice(0, preDigits)}...${wallet.slice(-(postDigits + 1), -1)}`; | export const truncateWallet: (wallet: string, preDigits: number, postDigits: number | undefined) => string = (wallet: string, preDigits: number, postDigits = preDigits) => `${wallet.slice(0, preDigits)}...${wallet.slice(-(postDigits + 1), -1)}`; | ||||
export const isNumber = (s: string) => /^[0-9]+$/.test(s); | export const isNumber = (s: string) => /^[0-9]+$/.test(s); |
@@ -5,11 +5,16 @@ import { | |||||
} from 'vue-router'; | } from 'vue-router'; | ||||
import RegisterView from '@/views/RegisterView.vue'; | import RegisterView from '@/views/RegisterView.vue'; | ||||
import LoginView from '@/views/LoginView.vue'; | import LoginView from '@/views/LoginView.vue'; | ||||
import { Privileges } from '@/api/types'; | |||||
import { | |||||
Privileges, | |||||
SuccessResponse, | |||||
} from '@/api/types'; | |||||
import HomeView from '@/views/HomeView.vue'; | import HomeView from '@/views/HomeView.vue'; | ||||
import FundView from '@/views/FundView.vue'; | import FundView from '@/views/FundView.vue'; | ||||
import AddFundView from '@/views/AddFundView.vue'; | import AddFundView from '@/views/AddFundView.vue'; | ||||
import hasPermission from '@/lib/auth'; | import hasPermission from '@/lib/auth'; | ||||
import SignetRequestController from '@/api/requests'; | |||||
import store from '@/store'; | |||||
const routes: Array<RouteRecordRaw> = [ | const routes: Array<RouteRecordRaw> = [ | ||||
{ | { | ||||
@@ -36,6 +41,11 @@ const routes: Array<RouteRecordRaw> = [ | |||||
component: RegisterView, | component: RegisterView, | ||||
meta: { | meta: { | ||||
requiredRights: Privileges.AdminPlus, | requiredRights: Privileges.AdminPlus, | ||||
accessible: async () => { | |||||
const controller = new SignetRequestController(store.getters.getToken); | |||||
const canProceed = await controller.post<SuccessResponse, null>('/UsersExist', null); | |||||
return canProceed?.success; | |||||
}, | |||||
title: 'Register', | title: 'Register', | ||||
}, | }, | ||||
}, | }, | ||||
@@ -58,7 +68,12 @@ const router = createRouter({ | |||||
router.beforeEach(async (to, from, next) => { | router.beforeEach(async (to, from, next) => { | ||||
document.title = `Beignet - ${to.meta.title}`; | document.title = `Beignet - ${to.meta.title}`; | ||||
if (hasPermission(to.meta.requiredRights as number)) { | |||||
const requiredRights = to.meta.requiredRights as number | undefined; | |||||
const accessible = to.meta.accessible as (() => SuccessResponse) | undefined; | |||||
const allowed = requiredRights ? hasPermission(requiredRights) | |||||
|| (accessible && !accessible().success) : true; | |||||
if (allowed) { | |||||
next(); | next(); | ||||
} else { | } else { | ||||
next('/'); | next('/'); | ||||
@@ -22,7 +22,7 @@ | |||||
</div> | </div> | ||||
<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="Fund Secret" aria-label="Fund Wallet" v-model="fundSecret"> | |||||
placeholder="Selling Wallet" aria-label="Selling Wallet" v-model="sellWallet"> | |||||
</div> | </div> | ||||
<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" | ||||
@@ -33,16 +33,17 @@ | |||||
<div class="title is-5 has-text-white-ter">Fund</div> | <div class="title is-5 has-text-white-ter">Fund</div> | ||||
<div class="control my-2 is-flex is-justify-content-space-between"> | <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" | <input class="input is-normal mr-1 has-background-white has-text-black" type="text" | ||||
placeholder="Asset" aria-label="Asset" v-model="asset"> | |||||
<input class="input is-normal ml-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"> | placeholder="Memo" aria-label="Memo" v-model="memo"> | ||||
</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="number" | |||||
placeholder="Min Contribution" aria-label="Asset" v-model="minContribution"> | |||||
<input class="input is-normal ml-1 has-background-white has-text-black" type="number" | <input class="input is-normal ml-1 has-background-white has-text-black" type="number" | ||||
placeholder="Amount Goal" aria-label="Memo" v-model="amtGoal"> | |||||
placeholder="Min Contribution" aria-label="Asset" v-model="minContribution"> | |||||
</div> | </div> | ||||
<!-- <div class="control my-2 is-flex is-justify-content-space-between">--> | |||||
<!-- <input class="input is-normal ml-1 has-background-white has-text-black" type="number"--> | |||||
<!-- placeholder="Amount Goal" aria-label="Memo" v-model="amtGoal">--> | |||||
<!-- </div>--> | |||||
</section> | </section> | ||||
<section class="section px-0 py-4"> | <section class="section px-0 py-4"> | ||||
<div class="title is-5 has-text-white-ter">Bonus Structure</div> | <div class="title is-5 has-text-white-ter">Bonus Structure</div> | ||||
@@ -81,12 +82,13 @@ const controller = new SignetRequestController(store.getters.getToken); | |||||
const title = ref(''); | const title = ref(''); | ||||
const description = ref(''); | const description = ref(''); | ||||
const fundWallet = ref(''); | const fundWallet = ref(''); | ||||
const fundSecret = ref(''); | |||||
const sellWallet = ref(''); | |||||
// const fundSecret = ref(''); | |||||
const issuerWallet = ref(''); | const issuerWallet = ref(''); | ||||
const asset = ref(''); | const asset = ref(''); | ||||
const memo = ref(''); | const memo = ref(''); | ||||
const minContribution = ref(undefined as number | undefined); | const minContribution = ref(undefined as number | undefined); | ||||
const amtGoal = ref(undefined as number | undefined); | |||||
// const amtGoal = ref(undefined as number | undefined); | |||||
const bonuses = ref([] as Bonus[]); | const bonuses = ref([] as Bonus[]); | ||||
const saveBonuses = (evt: Bonus[]) => { | const saveBonuses = (evt: Bonus[]) => { | ||||
@@ -100,10 +102,10 @@ const submit = async () => { | |||||
const resp = await controller.post<SuccessResponse, Partial<FundInfo>>('CreateRewardFund', { | const resp = await controller.post<SuccessResponse, Partial<FundInfo>>('CreateRewardFund', { | ||||
asset: asset.value, | asset: asset.value, | ||||
fundWallet: fundWallet.value, | fundWallet: fundWallet.value, | ||||
fundSecret: fundSecret.value, | |||||
sellingWallet: sellWallet.value, | |||||
issuerWallet: issuerWallet.value, | issuerWallet: issuerWallet.value, | ||||
memo: memo.value, | memo: memo.value, | ||||
amountGoal: amtGoal.value as number, | |||||
// amountGoal: amtGoal.value as number, | |||||
minContribution: minContribution.value as number, | minContribution: minContribution.value as number, | ||||
title: title.value, | title: title.value, | ||||
description: description.value, | description: description.value, | ||||
@@ -29,14 +29,6 @@ | |||||
<div class="title is-size-4 has-text-grey-dark has-text-centered"> | <div class="title is-size-4 has-text-grey-dark has-text-centered"> | ||||
Tracker | Tracker | ||||
</div> | </div> | ||||
<div class="has-text-centered is-size-4 has-text-grey-dark mb-3"> | |||||
<span class="total-label is-size-3 pr-2 has-text-weight-light"> | |||||
Total | |||||
</span> | |||||
<span class="pl-3 has-text-weight-bold"> | |||||
{{ fund.total.amountHeld.toLocaleString() }} XLM | |||||
</span> | |||||
</div> | |||||
<div class="level" v-if="fund.fundInfo.bonuses.length > 0"> | <div class="level" v-if="fund.fundInfo.bonuses.length > 0"> | ||||
<div class="level-item has-text-centered" | <div class="level-item has-text-centered" | ||||
v-for="bonus in fund.fundInfo.bonuses.sort((v1, v2) => v1.goal - v2.goal)" | v-for="bonus in fund.fundInfo.bonuses.sort((v1, v2) => v1.goal - v2.goal)" | ||||
@@ -45,14 +37,14 @@ | |||||
<div> | <div> | ||||
<p | <p | ||||
class="heading" | class="heading" | ||||
:class="fund.total.amountHeld >= bonus.goal | |||||
:class="amountHeld >= bonus.goal | |||||
? 'has-text-success' : 'has-text-grey-dark'" | ? 'has-text-success' : 'has-text-grey-dark'" | ||||
> | > | ||||
{{ bonus.goal.toLocaleString() }} XLM | {{ bonus.goal.toLocaleString() }} XLM | ||||
</p> | </p> | ||||
<p | <p | ||||
class="title" | class="title" | ||||
:class="fund.total.amountHeld >= bonus.goal | |||||
:class="amountHeld >= bonus.goal | |||||
? 'has-text-success' : 'has-text-grey-dark'" | ? 'has-text-success' : 'has-text-grey-dark'" | ||||
> | > | ||||
{{ bonus.percent }}% | {{ bonus.percent }}% | ||||
@@ -61,6 +53,31 @@ | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<div | |||||
class="is-flex | |||||
is-flex-direction-row | |||||
is-justify-content-space-around | |||||
is-size-4 | |||||
has-text-white | |||||
mb-3" | |||||
> | |||||
<div class="total"> | |||||
<span class="total-label is-size-3 pr-2 has-text-weight-light"> | |||||
Raised | |||||
</span> | |||||
<span class="pl-3 has-text-weight-bold"> | |||||
{{ amountHeld.toLocaleString() }} XLM | |||||
</span> | |||||
</div> | |||||
<div class="remaining"> | |||||
<span class="remaining-label is-size-3 pr-2 has-text-weight-light"> | |||||
Remaining | |||||
</span> | |||||
<span class="pl-3 has-text-weight-bold"> | |||||
{{ amountAvailable.toLocaleString() }} XLM | |||||
</span> | |||||
</div> | |||||
</div> | |||||
</section> | </section> | ||||
<section class="section is-small"> | <section class="section is-small"> | ||||
<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"> | ||||
@@ -78,6 +95,7 @@ | |||||
<button | <button | ||||
class="button is-primary" | class="button is-primary" | ||||
:class="requesting ? 'is-loading' : ''" | :class="requesting ? 'is-loading' : ''" | ||||
:disabled="amt > amountAvailable" | |||||
@click="makeContribution" | @click="makeContribution" | ||||
>Submit</button> | >Submit</button> | ||||
</div> | </div> | ||||
@@ -143,7 +161,7 @@ | |||||
<div class="box is-flex is-flex-direction-row is-justify-content-space-between"> | <div class="box is-flex is-flex-direction-row is-justify-content-space-between"> | ||||
<div class="my-auto"> | <div class="my-auto"> | ||||
<label class="checkbox" for="delete-confirm"> | <label class="checkbox" for="delete-confirm"> | ||||
<input type="checkbox" id="delete-confirm" v-model="allowDelete"> Delete | |||||
<input type="checkbox" id="delete-confirm" v-model="allowDelete"> Close | |||||
</label> | </label> | ||||
</div> | </div> | ||||
<div> | <div> | ||||
@@ -152,7 +170,7 @@ | |||||
:disabled="!allowDelete" | :disabled="!allowDelete" | ||||
@click="deleteFund" | @click="deleteFund" | ||||
> | > | ||||
Delete | |||||
Close | |||||
</button> | </button> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
@@ -254,7 +272,7 @@ fundDetails.value = [ | |||||
}, | }, | ||||
{ | { | ||||
title: 'Goal', | title: 'Goal', | ||||
val: `${fund.value.fundInfo.amountGoal.toLocaleString()}`, | |||||
val: `${fund.value.fundInfo.amountAvailable.toLocaleString()}`, | |||||
}, | }, | ||||
{ | { | ||||
title: 'Memo', | title: 'Memo', | ||||
@@ -270,21 +288,27 @@ if (fund.value.contributions.dates) { | |||||
const reward = ref(0); | const reward = ref(0); | ||||
const maxBonus = ref(0); | const maxBonus = ref(0); | ||||
const bonus = ref(undefined as Bonus | undefined); | const bonus = ref(undefined as Bonus | undefined); | ||||
const achievedBonuses = fund.value.fundInfo.bonuses.filter( | |||||
(b) => { | |||||
if (!fund.value) throw new Error('Fund not found'); | |||||
return b.goal && fund.value.total.amountHeld >= b.goal; | |||||
}, | |||||
); | |||||
if (achievedBonuses.length > 0) { | |||||
maxBonus.value = Math.max(...achievedBonuses.map((b) => b.goal ?? -1)); | |||||
bonus.value = achievedBonuses.find((b) => b.goal === maxBonus.value); | |||||
if (!Object.entries(bonus).length) throw new Error('Something went wrong'); | |||||
} | |||||
const amountHeld = ref(fund.value.total.amountHeld); | |||||
const amountAvailable = ref(fund.value.fundInfo.amountAvailable); | |||||
const calculateReward = (bought: number) => { | |||||
const getCurrentBonus = () => { | |||||
if (!fund.value) throw new Error('Fund not found!'); | |||||
const achievedBonuses = fund.value.fundInfo.bonuses.filter( | |||||
(b) => { | |||||
if (!fund.value) throw new Error('Fund not found'); | |||||
return b.goal && fund.value.total.amountHeld >= b.goal; | |||||
}, | |||||
); | |||||
if (achievedBonuses.length > 0) { | if (achievedBonuses.length > 0) { | ||||
if (!bonus.value || !bonus.value.percent) throw new Error('Something went wrong'); | |||||
maxBonus.value = Math.max(...achievedBonuses.map((b) => b.goal ?? -1)); | |||||
bonus.value = achievedBonuses.find((b) => b.goal === maxBonus.value); | |||||
if (!Object.entries(bonus).length) throw new Error('Something went wrong'); | |||||
} | |||||
}; | |||||
const calculateReward = (bought: number) => { | |||||
if (bonus.value) { | |||||
if (!bonus.value.percent) throw new Error('Bonus did not have percent for some reason'); | |||||
reward.value = bought + bought * (bonus.value.percent / 100); | reward.value = bought + bought * (bonus.value.percent / 100); | ||||
} else { | } else { | ||||
reward.value = bought; | reward.value = bought; | ||||
@@ -377,9 +401,14 @@ const { | |||||
watch(data, (newVal) => { | watch(data, (newVal) => { | ||||
if (!fund.value) throw new Error('Fund not found'); | if (!fund.value) throw new Error('Fund not found'); | ||||
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(); | 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 | 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); | ||||
@@ -388,19 +417,16 @@ watch(data, (newVal) => { | |||||
} else { | } else { | ||||
contributions.value.splice(0, 0, v); | contributions.value.splice(0, 0, v); | ||||
offset.value += 1; | offset.value += 1; | ||||
const formattedDate = formatDate(v.CreatedAt); | |||||
if (!selectableDates.value.includes(formattedDate)) { | |||||
selectableDates.value.push(formattedDate); | |||||
} | |||||
} | } | ||||
fund.value.total.amountHeld += v.amount; | |||||
amountHeld.value += v.amount; | |||||
amountAvailable.value -= v.amount; | |||||
} | } | ||||
}); | }); | ||||
const requesting = ref(false); | const requesting = ref(false); | ||||
const makeContribution = async () => { | const makeContribution = async () => { | ||||
if (!fund.value) throw new Error('Fund not found'); | if (!fund.value) throw new Error('Fund not found'); | ||||
if (!requesting.value && pk.value && amt.value) { | |||||
if (!requesting.value && pk.value && amt.value && amt.value <= amountAvailable.value) { | |||||
requesting.value = true; | requesting.value = true; | ||||
await controller.post<SuccessResponse, ContributeRequest>('Contribute', { | await controller.post<SuccessResponse, ContributeRequest>('Contribute', { | ||||
privateKey: pk.value, | privateKey: pk.value, | ||||
@@ -428,7 +454,7 @@ const makeContribution = async () => { | |||||
vertical-align: bottom; | vertical-align: bottom; | ||||
text-overflow: ellipsis; | text-overflow: ellipsis; | ||||
.total-label | |||||
.total-label, .remaining-label | |||||
border-right 1px solid #8f8f8f | border-right 1px solid #8f8f8f | ||||
font-variant all-petite-caps | font-variant all-petite-caps | ||||
@@ -1,13 +1,18 @@ | |||||
<template> | <template> | ||||
<div class="container is-max-desktop"> | <div class="container is-max-desktop"> | ||||
<section class="section is-small px-0"> | <section class="section is-small px-0"> | ||||
<div class="container-grid"> | |||||
<div class="container-grid" v-if="rewardFunds"> | |||||
<template v-for="fund in rewardFunds" v-bind:key="fund.id"> | <template v-for="fund in rewardFunds" v-bind:key="fund.id"> | ||||
<RouterLink :to="`/fund/${fund.id}`"> | <RouterLink :to="`/fund/${fund.id}`"> | ||||
<FundLink :fund="fund"/> | <FundLink :fund="fund"/> | ||||
</RouterLink> | </RouterLink> | ||||
</template> | </template> | ||||
</div> | </div> | ||||
<div v-else> | |||||
<div class="has-text-centered is-size-4"> | |||||
No group funds yet! | |||||
</div> | |||||
</div> | |||||
</section> | </section> | ||||
</div> | </div> | ||||
</template> | </template> | ||||
@@ -40,7 +40,7 @@ const submit = async () => { | |||||
if (!resp) throw new Error('Could not get response from registration'); | if (!resp) throw new Error('Could not get response from registration'); | ||||
success.value = resp.success; | success.value = resp.success; | ||||
if (success.value) { | if (success.value) { | ||||
await router.push('/login'); | |||||
await router.push('/'); | |||||
} | } | ||||
}; | }; | ||||
</script> | </script> | ||||
@@ -1,5 +1,16 @@ | |||||
const { defineConfig } = require('@vue/cli-service'); | const { defineConfig } = require('@vue/cli-service'); | ||||
module.exports = defineConfig({ | module.exports = defineConfig({ | ||||
devServer: { | |||||
proxy: { | |||||
'^/api': { | |||||
target: 'http://localhost:7300', | |||||
changeOrigin: true, | |||||
secure: false, | |||||
pathRewrite: { '^/api': '' }, | |||||
logLevel: 'debug', | |||||
}, | |||||
}, | |||||
}, | |||||
transpileDependencies: true, | transpileDependencies: true, | ||||
}); | }); |