Use router-view for markets
This commit is contained in:
parent
6b9dd419d4
commit
e84db076a4
20
db/market.go
20
db/market.go
|
@ -132,3 +132,23 @@ func (db *DB) FetchUserOrders(pubkey string, orders *[]Order) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) FetchMarketOrders(marketId int64, orders *[]Order) error {
|
||||
query := "" +
|
||||
"SELECT o.id, share_id, o.pubkey, o.side, o.quantity, o.price, o.invoice_id, o.created_at, s.description, s.market_id " +
|
||||
"FROM orders o " +
|
||||
"JOIN shares s ON o.share_id = s.id " +
|
||||
"WHERE s.market_id = $1 " +
|
||||
"ORDER BY o.created_at DESC"
|
||||
rows, err := db.Query(query, marketId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var order Order
|
||||
rows.Scan(&order.Id, &order.ShareId, &order.Pubkey, &order.Side, &order.Quantity, &order.Price, &order.InvoiceId, &order.CreatedAt, &order.ShareDescription, &order.Share.MarketId)
|
||||
*orders = append(*orders, order)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -112,6 +112,23 @@ func HandleCreateMarket(sc context.ServerContext) echo.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func HandleMarketOrders(sc context.ServerContext) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
marketId int64
|
||||
orders []db.Order
|
||||
err error
|
||||
)
|
||||
if marketId, err = strconv.ParseInt(c.Param("id"), 10, 64); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
|
||||
}
|
||||
if err = sc.Db.FetchMarketOrders(marketId, &orders); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.JSON(http.StatusOK, orders)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleOrder(sc context.ServerContext) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
|
|
|
@ -46,6 +46,7 @@ func addBackendRoutes(e *echo.Echo, sc ServerContext) {
|
|||
middleware.SessionGuard,
|
||||
middleware.LNDGuard)
|
||||
GET(e, sc, "/api/market/:id", handler.HandleMarket)
|
||||
GET(e, sc, "/api/market/:id/orders", handler.HandleMarketOrders)
|
||||
POST(e, sc, "/api/order",
|
||||
handler.HandleOrder,
|
||||
middleware.SessionGuard,
|
||||
|
|
|
@ -10,62 +10,24 @@
|
|||
</div>
|
||||
<div class="font-mono">{{ market.Description }}</div>
|
||||
<!-- eslint-enable -->
|
||||
<button type="button" :class="yesClass" class="label success font-mono mx-1 my-3"
|
||||
@click.prevent="toggleYes">YES</button>
|
||||
<button type="button" :class="noClass" class="label error font-mono mx-1 my-3" @click.prevent="toggleNo">NO</button>
|
||||
<form v-show="showForm" @submit.prevent="submitForm">
|
||||
<label for="stake">how much?</label>
|
||||
<input name="stake" v-model="stake" type="number" min="0" placeholder="sats" required />
|
||||
<label for="certainty">how sure?</label>
|
||||
<input name="certainty" v-model="certainty" type="number" min="0.01" max="0.99" step="0.01" required />
|
||||
<label>you receive:</label>
|
||||
<label>{{ format(shares) }} {{ selected }} shares @ {{ format(price) }} sats</label>
|
||||
<label>you pay:</label>
|
||||
<label>{{ format(cost) }} sats</label>
|
||||
<label>if you win:</label>
|
||||
<label>{{ format(profit) }} sats</label>
|
||||
<button class="col-span-2" type="submit">submit order</button>
|
||||
</form>
|
||||
<div v-if="err" class="red text-center">{{ err }}</div>
|
||||
<header class="flex flex-row text-center justify-center pt-1">
|
||||
<nav>
|
||||
<StyledLink :to="'/market/' + marketId + '/form'">form</StyledLink>
|
||||
<StyledLink :to="'/market/' + marketId + '/orders'">orders</StyledLink>
|
||||
</nav>
|
||||
</header>
|
||||
<Suspense>
|
||||
<router-view class="m-3" />
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import StyledLink from '@/components/StyledLink'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const marketId = route.params.id
|
||||
const selected = ref(null)
|
||||
const showForm = computed(() => selected.value !== null)
|
||||
const yesClass = computed(() => selected.value === 'YES' ? ['active'] : [])
|
||||
const noClass = computed(() => selected.value === 'NO' ? ['active'] : [])
|
||||
const err = ref(null)
|
||||
|
||||
// how much wants the user bet?
|
||||
const stake = ref(null)
|
||||
// how sure is the user he will win?
|
||||
const certainty = ref(null)
|
||||
// price per share: more risk, lower price, higher reward
|
||||
const price = computed(() => certainty.value * 100)
|
||||
// how many (full) shares can be bought?
|
||||
const shares = computed(() => {
|
||||
const val = price.value > 0 ? stake.value / price.value : null
|
||||
// only full shares can be bought
|
||||
return Math.round(val)
|
||||
})
|
||||
// how much does this order cost?
|
||||
const cost = computed(() => {
|
||||
return shares.value * price.value
|
||||
})
|
||||
// how high is the potential reward?
|
||||
const profit = computed(() => {
|
||||
// shares expire at 10 or 0 sats
|
||||
const val = (100 * shares.value) - cost.value
|
||||
return isNaN(val) ? 0 : val
|
||||
})
|
||||
|
||||
const format = (x, i = 3) => x === null ? null : x >= 1 ? Math.round(x) : x === 0 ? x : x.toFixed(i)
|
||||
|
||||
const market = ref(null)
|
||||
const url = '/api/market/' + marketId
|
||||
|
@ -75,66 +37,16 @@ await fetch(url)
|
|||
market.value = body
|
||||
})
|
||||
.catch(console.error)
|
||||
// Currently, we only support binary markets.
|
||||
// (only events which can be answered with YES and NO)
|
||||
const yesShareId = computed(() => {
|
||||
return market?.value.Shares.find(s => s.Description === 'YES').Id
|
||||
})
|
||||
const noShareId = computed(() => {
|
||||
return market?.value.Shares.find(s => s.Description === 'NO').Id
|
||||
})
|
||||
const shareId = computed(() => {
|
||||
return selected.value === 'YES' ? yesShareId.value : noShareId.value
|
||||
})
|
||||
|
||||
const toggleYes = () => {
|
||||
selected.value = selected.value === 'YES' ? null : 'YES'
|
||||
}
|
||||
|
||||
const toggleNo = () => {
|
||||
selected.value = selected.value === 'NO' ? null : 'NO'
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
// TODO validate form
|
||||
const url = window.origin + '/api/order'
|
||||
const body = JSON.stringify({
|
||||
sid: shareId.value,
|
||||
quantity: shares.value,
|
||||
price: price.value,
|
||||
// TODO support selling
|
||||
side: 'BUY'
|
||||
})
|
||||
const res = await fetch(url, { method: 'POST', headers: { 'Content-type': 'application/json' }, body })
|
||||
const resBody = await res.json()
|
||||
if (res.status !== 402) {
|
||||
err.value = `error: server responded with HTTP ${resBody.status}`
|
||||
return
|
||||
}
|
||||
const invoiceId = resBody.id
|
||||
router.push('/invoice/' + invoiceId)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.success.active {
|
||||
background-color: #35df8d;
|
||||
color: white;
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.error.active {
|
||||
background-color: #ff7386;
|
||||
color: white;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
form>* {
|
||||
margin: 0.5em 1em;
|
||||
nav>a {
|
||||
margin: 0 3px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div class="text w-auto">
|
||||
<table>
|
||||
<thead>
|
||||
<th>description</th>
|
||||
<th class="hidden-sm">created at</th>
|
||||
<th>status</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="o in orders " :key="o.id" class="success">
|
||||
<td>{{ o.side }} {{ o.quantity }} {{ o.ShareDescription }} @ {{ o.price }} sats</td>
|
||||
<td class="hidden-sm">{{ ago(new Date(o.CreatedAt)) }}</td>
|
||||
<td>PENDING</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import ago from 's-ago'
|
||||
|
||||
const route = useRoute()
|
||||
const marketId = route.params.id
|
||||
|
||||
const orders = ref([])
|
||||
const url = `/api/market/${marketId}/orders`
|
||||
await fetch(url)
|
||||
.then(r => r.json())
|
||||
.then(body => {
|
||||
orders.value = body
|
||||
})
|
||||
.catch(console.error)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
table {
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
th {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.hidden-sm {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,129 @@
|
|||
<template>
|
||||
<button type="button" :class="yesClass" class="label success font-mono mx-1 my-3"
|
||||
@click.prevent="toggleYes">YES</button>
|
||||
<button type="button" :class="noClass" class="label error font-mono mx-1 my-3" @click.prevent="toggleNo">NO</button>
|
||||
<form v-show="showForm" @submit.prevent="submitForm">
|
||||
<label for="stake">how much?</label>
|
||||
<input name="stake" v-model="stake" type="number" min="0" placeholder="sats" required />
|
||||
<label for="certainty">how sure?</label>
|
||||
<input name="certainty" v-model="certainty" type="number" min="0.01" max="0.99" step="0.01" required />
|
||||
<label>you receive:</label>
|
||||
<label>{{ format(shares) }} {{ selected }} shares @ {{ format(price) }} sats</label>
|
||||
<label>you pay:</label>
|
||||
<label>{{ format(cost) }} sats</label>
|
||||
<label>if you win:</label>
|
||||
<label>{{ format(profit) }} sats</label>
|
||||
<button class="col-span-2" type="submit">submit order</button>
|
||||
</form>
|
||||
<div v-if="err" class="red text-center">{{ err }}</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const marketId = route.params.id
|
||||
const selected = ref(null)
|
||||
const showForm = computed(() => selected.value !== null)
|
||||
const yesClass = computed(() => selected.value === 'YES' ? ['active'] : [])
|
||||
const noClass = computed(() => selected.value === 'NO' ? ['active'] : [])
|
||||
const err = ref(null)
|
||||
|
||||
// how much wants the user bet?
|
||||
const stake = ref(null)
|
||||
// how sure is the user he will win?
|
||||
const certainty = ref(null)
|
||||
// price per share: more risk, lower price, higher reward
|
||||
const price = computed(() => certainty.value * 100)
|
||||
// how many (full) shares can be bought?
|
||||
const shares = computed(() => {
|
||||
const val = price.value > 0 ? stake.value / price.value : null
|
||||
// only full shares can be bought
|
||||
return Math.round(val)
|
||||
})
|
||||
// how much does this order cost?
|
||||
const cost = computed(() => {
|
||||
return shares.value * price.value
|
||||
})
|
||||
// how high is the potential reward?
|
||||
const profit = computed(() => {
|
||||
// shares expire at 10 or 0 sats
|
||||
const val = (100 * shares.value) - cost.value
|
||||
return isNaN(val) ? 0 : val
|
||||
})
|
||||
|
||||
const format = (x, i = 3) => x === null ? null : x >= 1 ? Math.round(x) : x === 0 ? x : x.toFixed(i)
|
||||
|
||||
const market = ref(null)
|
||||
const url = '/api/market/' + marketId
|
||||
await fetch(url)
|
||||
.then(r => r.json())
|
||||
.then(body => {
|
||||
market.value = body
|
||||
})
|
||||
.catch(console.error)
|
||||
// Currently, we only support binary markets.
|
||||
// (only events which can be answered with YES and NO)
|
||||
const yesShareId = computed(() => {
|
||||
return market?.value.Shares.find(s => s.Description === 'YES').Id
|
||||
})
|
||||
const noShareId = computed(() => {
|
||||
return market?.value.Shares.find(s => s.Description === 'NO').Id
|
||||
})
|
||||
const shareId = computed(() => {
|
||||
return selected.value === 'YES' ? yesShareId.value : noShareId.value
|
||||
})
|
||||
|
||||
const toggleYes = () => {
|
||||
selected.value = selected.value === 'YES' ? null : 'YES'
|
||||
}
|
||||
|
||||
const toggleNo = () => {
|
||||
selected.value = selected.value === 'NO' ? null : 'NO'
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
// TODO validate form
|
||||
const url = window.origin + '/api/order'
|
||||
const body = JSON.stringify({
|
||||
sid: shareId.value,
|
||||
quantity: shares.value,
|
||||
price: price.value,
|
||||
// TODO support selling
|
||||
side: 'BUY'
|
||||
})
|
||||
const res = await fetch(url, { method: 'POST', headers: { 'Content-type': 'application/json' }, body })
|
||||
const resBody = await res.json()
|
||||
if (res.status !== 402) {
|
||||
err.value = `error: server responded with HTTP ${resBody.status}`
|
||||
return
|
||||
}
|
||||
const invoiceId = resBody.id
|
||||
router.push('/invoice/' + invoiceId)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.success.active {
|
||||
background-color: #35df8d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.error.active {
|
||||
background-color: #ff7386;
|
||||
color: white;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
form>* {
|
||||
margin: 0.5em 1em;
|
||||
}
|
||||
</style>
|
|
@ -50,6 +50,7 @@ a.selected {
|
|||
|
||||
.success:hover {
|
||||
background-color: #35df8d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.red {
|
||||
|
@ -63,6 +64,7 @@ a.selected {
|
|||
|
||||
.error:hover {
|
||||
background-color: #ff7386;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
|
|
|
@ -13,6 +13,8 @@ import InvoiceView from '@/views/InvoiceView'
|
|||
import UserSettings from '@/components/UserSettings'
|
||||
import UserInvoices from '@/components/UserInvoices'
|
||||
import UserOrders from '@/components/UserOrders'
|
||||
import OrderForm from '@/components/OrderForm'
|
||||
import MarketOrders from '@/components/MarketOrders'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
|
@ -31,7 +33,12 @@ const routes = [
|
|||
]
|
||||
},
|
||||
{
|
||||
path: '/market/:id', component: MarketView
|
||||
path: '/market/:id',
|
||||
component: MarketView,
|
||||
children: [
|
||||
{ path: 'form', name: 'form', component: OrderForm },
|
||||
{ path: 'orders', name: 'market-orders', component: MarketOrders }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/invoice/:id', component: InvoiceView
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</nav>
|
||||
</header>
|
||||
<Suspense>
|
||||
<router-view />
|
||||
<router-view class="m-3" />
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
|
|
Loading…
Reference in New Issue