@@ -43,6 +43,12 @@ | |||
</Suspense> | |||
</RouterView> | |||
</div> | |||
<footer> | |||
<div> | |||
Proudly made in Michigan <div class="michigan-icon"></div> | |||
</div> | |||
</footer> | |||
</template> | |||
<script setup lang="ts"> | |||
@@ -85,4 +91,23 @@ body | |||
min-height 100vh | |||
color #e8dbca | |||
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> |
@@ -61,10 +61,11 @@ export interface FundInfo { | |||
id: number; | |||
asset: string; | |||
fundWallet: string; | |||
fundSecret: string; | |||
sellingWallet: string; | |||
issuerWallet: string; | |||
memo: string; | |||
price: number; | |||
amountAvailable: number; | |||
amountGoal: number; | |||
minContribution: number; | |||
title: string; | |||
@@ -12,7 +12,7 @@ | |||
</li> | |||
<li class="is-size-7 is-inline-block px-2"> | |||
<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 class="is-size-7 is-inline-block px-2"> | |||
<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 isNumber = (s: string) => /^[0-9]+$/.test(s); |
@@ -5,11 +5,16 @@ import { | |||
} from 'vue-router'; | |||
import RegisterView from '@/views/RegisterView.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 FundView from '@/views/FundView.vue'; | |||
import AddFundView from '@/views/AddFundView.vue'; | |||
import hasPermission from '@/lib/auth'; | |||
import SignetRequestController from '@/api/requests'; | |||
import store from '@/store'; | |||
const routes: Array<RouteRecordRaw> = [ | |||
{ | |||
@@ -36,6 +41,11 @@ const routes: Array<RouteRecordRaw> = [ | |||
component: RegisterView, | |||
meta: { | |||
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', | |||
}, | |||
}, | |||
@@ -58,7 +68,12 @@ const router = createRouter({ | |||
router.beforeEach(async (to, from, next) => { | |||
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(); | |||
} else { | |||
next('/'); | |||
@@ -22,7 +22,7 @@ | |||
</div> | |||
<div class="control my-2"> | |||
<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 class="control my-2"> | |||
<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="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" 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"> | |||
</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" | |||
placeholder="Amount Goal" aria-label="Memo" v-model="amtGoal"> | |||
placeholder="Min Contribution" aria-label="Asset" v-model="minContribution"> | |||
</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 class="section px-0 py-4"> | |||
<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 description = ref(''); | |||
const fundWallet = ref(''); | |||
const fundSecret = ref(''); | |||
const sellWallet = ref(''); | |||
// const fundSecret = ref(''); | |||
const issuerWallet = ref(''); | |||
const asset = ref(''); | |||
const memo = ref(''); | |||
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 saveBonuses = (evt: Bonus[]) => { | |||
@@ -100,10 +102,10 @@ const submit = async () => { | |||
const resp = await controller.post<SuccessResponse, Partial<FundInfo>>('CreateRewardFund', { | |||
asset: asset.value, | |||
fundWallet: fundWallet.value, | |||
fundSecret: fundSecret.value, | |||
sellingWallet: sellWallet.value, | |||
issuerWallet: issuerWallet.value, | |||
memo: memo.value, | |||
amountGoal: amtGoal.value as number, | |||
// amountGoal: amtGoal.value as number, | |||
minContribution: minContribution.value as number, | |||
title: title.value, | |||
description: description.value, | |||
@@ -29,14 +29,6 @@ | |||
<div class="title is-size-4 has-text-grey-dark has-text-centered"> | |||
Tracker | |||
</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-item has-text-centered" | |||
v-for="bonus in fund.fundInfo.bonuses.sort((v1, v2) => v1.goal - v2.goal)" | |||
@@ -45,14 +37,14 @@ | |||
<div> | |||
<p | |||
class="heading" | |||
:class="fund.total.amountHeld >= bonus.goal | |||
:class="amountHeld >= bonus.goal | |||
? 'has-text-success' : 'has-text-grey-dark'" | |||
> | |||
{{ bonus.goal.toLocaleString() }} XLM | |||
</p> | |||
<p | |||
class="title" | |||
:class="fund.total.amountHeld >= bonus.goal | |||
:class="amountHeld >= bonus.goal | |||
? 'has-text-success' : 'has-text-grey-dark'" | |||
> | |||
{{ bonus.percent }}% | |||
@@ -61,6 +53,31 @@ | |||
</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 class="section is-small"> | |||
<div class="title is-size-4 has-text-white-ter has-text-centered"> | |||
@@ -78,6 +95,7 @@ | |||
<button | |||
class="button is-primary" | |||
:class="requesting ? 'is-loading' : ''" | |||
:disabled="amt > amountAvailable" | |||
@click="makeContribution" | |||
>Submit</button> | |||
</div> | |||
@@ -143,7 +161,7 @@ | |||
<div class="box is-flex is-flex-direction-row is-justify-content-space-between"> | |||
<div class="my-auto"> | |||
<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> | |||
</div> | |||
<div> | |||
@@ -152,7 +170,7 @@ | |||
:disabled="!allowDelete" | |||
@click="deleteFund" | |||
> | |||
Delete | |||
Close | |||
</button> | |||
</div> | |||
</div> | |||
@@ -254,7 +272,7 @@ fundDetails.value = [ | |||
}, | |||
{ | |||
title: 'Goal', | |||
val: `${fund.value.fundInfo.amountGoal.toLocaleString()}`, | |||
val: `${fund.value.fundInfo.amountAvailable.toLocaleString()}`, | |||
}, | |||
{ | |||
title: 'Memo', | |||
@@ -270,21 +288,27 @@ if (fund.value.contributions.dates) { | |||
const reward = ref(0); | |||
const maxBonus = ref(0); | |||
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 (!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); | |||
} else { | |||
reward.value = bought; | |||
@@ -377,9 +401,14 @@ const { | |||
watch(data, (newVal) => { | |||
if (!fund.value) throw new Error('Fund not found'); | |||
getCurrentBonus(); | |||
if (status.value === 'OPEN') { | |||
const v = JSON.parse(newVal.trim()) as Contribution; | |||
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)) { | |||
const hasContribution = contributions.value.find((c: Contribution) => c.wallet === v.wallet); | |||
@@ -388,19 +417,16 @@ watch(data, (newVal) => { | |||
} else { | |||
contributions.value.splice(0, 0, v); | |||
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 makeContribution = async () => { | |||
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; | |||
await controller.post<SuccessResponse, ContributeRequest>('Contribute', { | |||
privateKey: pk.value, | |||
@@ -428,7 +454,7 @@ const makeContribution = async () => { | |||
vertical-align: bottom; | |||
text-overflow: ellipsis; | |||
.total-label | |||
.total-label, .remaining-label | |||
border-right 1px solid #8f8f8f | |||
font-variant all-petite-caps | |||
@@ -1,13 +1,18 @@ | |||
<template> | |||
<div class="container is-max-desktop"> | |||
<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"> | |||
<RouterLink :to="`/fund/${fund.id}`"> | |||
<FundLink :fund="fund"/> | |||
</RouterLink> | |||
</template> | |||
</div> | |||
<div v-else> | |||
<div class="has-text-centered is-size-4"> | |||
No group funds yet! | |||
</div> | |||
</div> | |||
</section> | |||
</div> | |||
</template> | |||
@@ -40,7 +40,7 @@ const submit = async () => { | |||
if (!resp) throw new Error('Could not get response from registration'); | |||
success.value = resp.success; | |||
if (success.value) { | |||
await router.push('/login'); | |||
await router.push('/'); | |||
} | |||
}; | |||
</script> | |||
@@ -1,5 +1,16 @@ | |||
const { defineConfig } = require('@vue/cli-service'); | |||
module.exports = defineConfig({ | |||
devServer: { | |||
proxy: { | |||
'^/api': { | |||
target: 'http://localhost:7300', | |||
changeOrigin: true, | |||
secure: false, | |||
pathRewrite: { '^/api': '' }, | |||
logLevel: 'debug', | |||
}, | |||
}, | |||
}, | |||
transpileDependencies: true, | |||
}); |