Compare commits

..

3 Commits

Author SHA1 Message Date
ekzyis ecee2f2a46 Load chart data from db 2024-09-09 01:58:09 +02:00
ekzyis 00ee710246 Show line chart with sample data 2024-09-09 00:47:42 +02:00
ekzyis bdc768cb27 Fix copy button flicker on poll 2024-09-09 00:47:10 +02:00
9 changed files with 218 additions and 37 deletions

4
.gitignore vendored
View File

@ -13,3 +13,7 @@ public/hotreload
# tailwindcss
public/css/tailwind.css
# js
node_modules
public/js/*.min.js

51
package-lock.json generated Normal file
View File

@ -0,0 +1,51 @@
{
"name": "delphi.market",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"chart.js": "^4.4.4",
"chartjs-adapter-dayjs-4": "^1.0.4",
"dayjs": "^1.11.13"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==",
"license": "MIT"
},
"node_modules/chart.js": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz",
"integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chartjs-adapter-dayjs-4": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/chartjs-adapter-dayjs-4/-/chartjs-adapter-dayjs-4-1.0.4.tgz",
"integrity": "sha512-yy9BAYW4aNzPVrCWZetbILegTRb7HokhgospPoC3b5iZ5qdlqNmXts2KdSp6AqnjkPAp/YWyHDxLvIvwt5x81w==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"chart.js": ">=4.0.1",
"dayjs": "^1.9.7"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
}
}
}

10
package.json Normal file
View File

@ -0,0 +1,10 @@
{
"scripts": {
"build": "esbuild public/js/chart.js --bundle --minify --outfile=public/js/chart.min.js"
},
"dependencies": {
"chart.js": "^4.4.4",
"chartjs-adapter-dayjs-4": "^1.0.4",
"dayjs": "^1.11.13"
}
}

65
public/js/chart.js Normal file
View File

@ -0,0 +1,65 @@
import {
Chart,
LineController,
TimeScale,
LinearScale,
PointElement,
LineElement,
} from 'chart.js'
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
Chart.register(
LineController,
TimeScale,
LinearScale,
PointElement,
LineElement,
);
const element = document.getElementById('chart')
function transformPoint({ X, Y }) {
return { x: new Date(X), y: Y * 100 }
}
const no = JSON.parse($("#chart-data").getAttribute("chart-data-p0")).map(transformPoint)
const yes = JSON.parse($("#chart-data").getAttribute("chart-data-p1")).map(transformPoint)
const config = {
type: 'line',
data: {
datasets: [
{
data: yes,
backgroundColor: '#149e613d',
borderColor: '#149e613d',
borderWidth: 3,
tension: 0, // draw straight lines instead of bezier curves
pointStyle: false
},
{
data: no,
backgroundColor: '#f5395e3d',
borderColor: '#f5395e3d',
borderWidth: 3,
tension: 0,
pointStyle: false
}
]
},
options: {
scales: {
x: {
type: 'time',
time: { unit: 'month' }
},
y: {
min: 0,
max: 100
}
}
}
}
new Chart(element, config);

View File

@ -113,6 +113,9 @@ func HandleMarket(sc context.Context) echo.HandlerFunc {
quote1 = types.MarketQuote{}
uQ0 int
uQ1 int
rows *sql.Rows
p0 []types.MarketPoint
p1 []types.MarketPoint
err error
)
@ -172,6 +175,38 @@ func HandleMarket(sc context.Context) echo.HandlerFunc {
return err
}
if rows, err = db.QueryContext(ctx, ""+
"SELECT created_at, quote(b, q0, q1, 1) AS p0, quote(b, q1, q0, 1) AS p1 "+
"FROM ( "+
" SELECT "+
" m.lmsr_b AS b, o.created_at, "+
" COALESCE(SUM(quantity) FILTER(WHERE o.outcome = 0) OVER (ORDER BY o.created_at ASC), 0) AS q0, "+
" COALESCE(sum(quantity) filter(where o.outcome = 1) over (order by o.created_at ASC), 0) AS q1 "+
" FROM markets m "+
" JOIN orders o ON o.market_id = m.id "+
" JOIN invoices i ON i.id = o.invoice_id "+
" WHERE m.id = $1 AND i.confirmed_at IS NOT NULL "+
") AS o "+
"UNION "+
"SELECT m.created_at, quote(m.lmsr_b, 0, 0, 1) AS p0, quote(m.lmsr_b, 0, 0, 1) AS p1 "+
"FROM markets m "+
"ORDER BY created_at", id); err != nil {
return err
}
for rows.Next() {
var (
createdAt time.Time
_p0 float64
_p1 float64
)
if err = rows.Scan(&createdAt, &_p0, &_p1); err != nil {
return err
}
p0 = append(p0, types.MarketPoint{X: createdAt, Y: _p0})
p1 = append(p1, types.MarketPoint{X: createdAt, Y: _p1})
}
total = lmsr.Quote(l.B, l.Q1, l.Q2, int(q))
quote0 = types.MarketQuote{
Outcome: 0,
@ -188,7 +223,11 @@ func HandleMarket(sc context.Context) echo.HandlerFunc {
Reward: float64(q) - total,
}
return pages.Market(m, quote0, quote1, uQ0, uQ1).Render(context.RenderContext(sc, c), c.Response().Writer)
return pages.Market(
m,
p0, p1,
quote0, quote1,
uQ0, uQ1).Render(context.RenderContext(sc, c), c.Response().Writer)
}
}

View File

@ -11,8 +11,8 @@ templ Invoice(hash string, bolt11 string, msats int, expiresIn int, paid bool, r
</div>
<div class="my-1">{ format(msats) }</div>
@InvoiceStatus(hash, expiresIn, paid, redirectUrl)
<div class="none" id="bolt11-data" bolt11-data={ templ.JSONString(bolt11) } hx-preserve></div>
<script type="text/javascript" id="bolt11-js" hx-preserve>
<div class="none" id="bolt11-data" bolt11-data={ templ.JSONString(bolt11) }></div>
<script type="text/javascript" id="bolt11-js">
htmx.on("#close", "click", function () {
// abort in-flight polls and prevent new polls
htmx.trigger("#poll", "htmx:abort")
@ -23,25 +23,26 @@ templ Invoice(hash string, bolt11 string, msats int, expiresIn int, paid bool, r
}
templ InvoiceStatus(hash string, expiresIn int, paid bool, redirectUrl templ.SafeURL) {
if paid {
<div class="font-mono neon success my-1">PAID</div>
<!-- TODO: show timer for redirect -->
<div
id="poll"
hx-get={ string(redirectUrl) }
hx-trigger="load delay:3s"
hx-target="#content"
hx-swap="outerHTML"
hx-select="#content"
hx-push-url="true"
hx-select-oob="#modal"
></div>
} else if expiresIn <= 0 {
<div class="font-mono neon error my-1">EXPIRED</div>
} else {
<!-- invoice is pending -->
<div class="font-mono my-1" id="countdown" countdown-data={ templ.JSONString(expiresIn) } hx-preserve></div>
<script type="text/javascript" id="countdown-js" hx-preserve>
<div id="status">
if paid {
<div class="font-mono neon success my-1">PAID</div>
<!-- TODO: show timer for redirect -->
<div
id="poll"
hx-get={ string(redirectUrl) }
hx-trigger="load delay:3s"
hx-target="#content"
hx-swap="outerHTML"
hx-select="#content"
hx-push-url="true"
hx-select-oob="#modal"
></div>
} else if expiresIn <= 0 {
<div class="font-mono neon error my-1">EXPIRED</div>
} else {
<!-- invoice is pending -->
<div class="font-mono my-1" id="countdown" countdown-data={ templ.JSONString(expiresIn) } hx-preserve></div>
<script type="text/javascript" id="countdown-js" hx-preserve>
var expiresIn = JSON.parse($("#countdown").getAttribute("countdown-data"))
function pad(num, places) {
@ -64,15 +65,16 @@ templ InvoiceStatus(hash string, expiresIn int, paid bool, redirectUrl templ.Saf
_countdown()
var interval = setInterval(_countdown, 1000)
</script>
<div
id="poll"
hx-get={ string(templ.SafeURL("/invoice/" + hash)) }
hx-trigger="load delay:1s"
hx-target="#modal"
hx-swap="outerHTML"
hx-select="#modal"
></div>
}
<div
id="poll"
hx-get={ string(templ.SafeURL("/invoice/" + hash)) }
hx-trigger="load delay:1s"
hx-target="#status"
hx-swap="outerHTML"
hx-select="#status"
></div>
}
</div>
}
func format(msats int) string {

View File

@ -13,7 +13,7 @@ templ Qr(value string, href string) {
>
<img src={ "data:image/jpeg;base64," + qrEncode(value) }/>
</a>
<small class="flex my-1 mx-auto">
<small class="flex my-1 mx-auto" hx-preserve>
<span class="block w-[188px] overflow-hidden">{ value }</span>
<button id="copy" class="ms-1 button w-[64px]" hx-preserve>copy</button>
</small>

View File

@ -7,7 +7,7 @@ import (
)
// TODO: Add countdown? Use or at least show somewhere precise timestamps?
templ Market(m types.Market, q0 types.MarketQuote, q1 types.MarketQuote, uQ0 int, uQ1 int) {
templ Market(m types.Market, p0 []types.MarketPoint, p1 []types.MarketPoint, q0 types.MarketQuote, q1 types.MarketQuote, uQ0 int, uQ1 int) {
<html>
@components.Head()
<body
@ -17,12 +17,17 @@ templ Market(m types.Market, q0 types.MarketQuote, q1 types.MarketQuote, uQ0 int
>
@components.Nav()
<div id="content" class="flex flex-col">
<small>
@components.Figlet("random", "market")
</small>
<div class="text-center font-bold my-1">{ m.Question }</div>
<div class="text-center font-bold my-1 mt-3">{ m.Question }</div>
<div class="text-center text-muted my-1">{ humanize.Time(m.EndDate) }</div>
<div class="text-center text-muted my-1"></div>
<div
class="none"
id="chart-data"
chart-data-p0={ templ.JSONString(p0) }
chart-data-p1={ templ.JSONString(p1) }
></div>
<div class="flex justify-center w-full max-h-64 my-3"><canvas id="chart"></canvas></div>
<script src="/js/chart.min.js"></script>
<blockquote class="p-4 mb-4 border-s-4 border-muted">
if m.Description != "" {
{ m.Description }

View File

@ -50,3 +50,8 @@ type MarketQuote struct {
TotalPrice float64
Reward float64
}
type MarketPoint struct {
X time.Time
Y float64
}