Compare commits

...

6 Commits

Author SHA1 Message Date
ekzyis ed9d6f30c4 Add TODOs 2024-09-09 00:35:15 +02:00
ekzyis 2e3e4f02b4 Use htmx.on 2024-09-05 23:33:23 +02:00
ekzyis 76bc50355a Define jQuery helper in head 2024-09-05 23:20:07 +02:00
ekzyis e4da49b215 Set min date 2024-09-05 23:12:34 +02:00
ekzyis 9df7ce8338 Fix missing formatting 2024-09-05 23:11:29 +02:00
ekzyis 9296bd5866 Remove cite=ekzyis from blockquote 2024-09-05 23:07:44 +02:00
9 changed files with 140 additions and 122 deletions

View File

@ -2,6 +2,17 @@ package pages
import "git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components" import "git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
/**
* TODO: explain how delphi.market works:
* - how to create a market
* - why you need to pay for a market
* - how the outcome of a market is determined (oracle model)
* - how (avg) price is calculated (LMSR)
* - what shares are and how much they are worth
* - risks: censorship, custodial model etc.
* - future plans
*/
templ About() { templ About() {
<html> <html>
@components.Head() @components.Head()

View File

@ -28,5 +28,9 @@ templ Head() {
if ctx.Value(c.EnvContextKey) == "development" { if ctx.Value(c.EnvContextKey) == "development" {
<script defer src="/js/hotreload.js"></script> <script defer src="/js/hotreload.js"></script>
} }
<script type="text/javascript">
// helper functions
var $ = selector => document.querySelector(selector)
</script>
</head> </head>
} }

View File

@ -1,48 +1,47 @@
package components package components
import ( import "fmt"
"fmt"
)
templ Invoice(hash string, bolt11 string, msats int, expiresIn int, paid bool, redirectUrl templ.SafeURL) { templ Invoice(hash string, bolt11 string, msats int, expiresIn int, paid bool, redirectUrl templ.SafeURL) {
<div class="p-5 border border-muted bg-background text-center font-mono"> <div class="p-5 border border-muted bg-background text-center font-mono">
<div id="close" class="flex justify-end"><button class="w-fit text-muted hitbox hover:text-reset">X</button></div> <div id="close" class="flex justify-end"><button class="w-fit text-muted hitbox hover:text-reset">X</button></div>
<div>Payment Required</div> <div>Payment Required</div>
<div class="my-1">@Qr(bolt11, "lightning:"+bolt11)</div> <div class="my-1">
<div class="my-1">{ format(msats) }</div> @Qr(bolt11, "lightning:"+bolt11)
@InvoiceStatus(hash, expiresIn, paid, redirectUrl) </div>
<div class="none" id="bolt11-data" bolt11-data={ templ.JSONString(bolt11) } hx-preserve></div> <div class="my-1">{ format(msats) }</div>
<script type="text/javascript" id="bolt11-js" hx-preserve> @InvoiceStatus(hash, expiresIn, paid, redirectUrl)
var $ = selector => document.querySelector(selector) <div class="none" id="bolt11-data" bolt11-data={ templ.JSONString(bolt11) } hx-preserve></div>
$("#close").addEventListener("click", function () { <script type="text/javascript" id="bolt11-js" hx-preserve>
htmx.on("#close", "click", function () {
// abort in-flight polls and prevent new polls // abort in-flight polls and prevent new polls
htmx.trigger("#poll", "htmx:abort") htmx.trigger("#poll", "htmx:abort")
$("#poll").addEventListener("htmx:beforeRequest", e => e.preventDefault()) htmx.on("#poll", "htmx:beforeRequest", function (e) { e.preventDefault() })
}) })
</script> </script>
</div> </div>
} }
templ InvoiceStatus(hash string, expiresIn int, paid bool, redirectUrl templ.SafeURL) { templ InvoiceStatus(hash string, expiresIn int, paid bool, redirectUrl templ.SafeURL) {
if paid { if paid {
<div class="font-mono neon success my-1">PAID</div> <div class="font-mono neon success my-1">PAID</div>
<div <!-- TODO: show timer for redirect -->
id="poll" <div
hx-get={ string(redirectUrl) } id="poll"
hx-trigger="load delay:3s" hx-get={ string(redirectUrl) }
hx-target="#content" hx-trigger="load delay:3s"
hx-swap="outerHTML" hx-target="#content"
hx-select="#content" hx-swap="outerHTML"
hx-push-url="true" hx-select="#content"
hx-select-oob="#modal" /> hx-push-url="true"
} hx-select-oob="#modal"
else if expiresIn <= 0 { ></div>
<div class="font-mono neon error my-1">EXPIRED</div> } else if expiresIn <= 0 {
} else { <div class="font-mono neon error my-1">EXPIRED</div>
<!-- invoice is pending --> } else {
<div class="font-mono my-1" id="countdown" countdown-data={ templ.JSONString(expiresIn) } hx-preserve></div> <!-- invoice is pending -->
<script type="text/javascript" id="countdown-js" hx-preserve> <div class="font-mono my-1" id="countdown" countdown-data={ templ.JSONString(expiresIn) } hx-preserve></div>
var $ = selector => document.querySelector(selector) <script type="text/javascript" id="countdown-js" hx-preserve>
var expiresIn = JSON.parse($("#countdown").getAttribute("countdown-data")) var expiresIn = JSON.parse($("#countdown").getAttribute("countdown-data"))
function pad(num, places) { function pad(num, places) {
@ -65,20 +64,21 @@ templ InvoiceStatus(hash string, expiresIn int, paid bool, redirectUrl templ.Saf
_countdown() _countdown()
var interval = setInterval(_countdown, 1000) var interval = setInterval(_countdown, 1000)
</script> </script>
<div <div
id="poll" id="poll"
hx-get={ string(templ.SafeURL("/invoice/" + hash)) } hx-get={ string(templ.SafeURL("/invoice/" + hash)) }
hx-trigger="load delay:1s" hx-trigger="load delay:1s"
hx-target="#modal" hx-target="#modal"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-select="#modal" /> hx-select="#modal"
} ></div>
}
} }
func format(msats int) string { func format(msats int) string {
sats := msats / 1000 sats := msats / 1000
if sats == 1 { if sats == 1 {
return fmt.Sprintf("%d sat", sats) return fmt.Sprintf("%d sat", sats)
} }
return fmt.Sprintf("%d sats", sats) return fmt.Sprintf("%d sats", sats)
} }

View File

@ -1,65 +1,64 @@
package components package components
import ( import (
"git.ekzyis.com/ekzyis/delphi.market/types" "fmt"
"git.ekzyis.com/ekzyis/delphi.market/types"
"fmt" "strconv"
"strconv"
) )
templ MarketForm(m types.Market, outcome int, q types.MarketQuote, uQ int) { templ MarketForm(m types.Market, outcome int, q types.MarketQuote, uQ int) {
<form <form
id={ formId(outcome) } id={ formId(outcome) }
autocomplete="off" autocomplete="off"
class="grid grid-cols-2 gap-3" class="grid grid-cols-2 gap-3"
hx-post={ fmt.Sprintf("/market/%d/order", m.Id) } hx-post={ fmt.Sprintf("/market/%d/order", m.Id) }
hx-target="#modal" hx-target="#modal"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-select="#modal" hx-select="#modal"
> >
<input type="hidden" name="o" value={ fmt.Sprint(outcome) } /> <input type="hidden" name="o" value={ fmt.Sprint(outcome) }/>
<div class="none col-span-2 htmx-request" /> <div class="none col-span-2 htmx-request"></div>
<label for="p">avg price per share:</label> <label for="p">avg price per share:</label>
<div id="p">{formatPrice(q.AvgPrice)}</div> <div id="p">{ formatPrice(q.AvgPrice) }</div>
<label for="q">how many?</label> <label for="q">how many?</label>
<input <input
id={ inputId(outcome) } id={ inputId(outcome) }
name="q" name="q"
class="text-black px-1" class="text-black px-1"
type="number" type="number"
autofocus autofocus
hx-get={ fmt.Sprintf("/market/%d", m.Id) } hx-get={ fmt.Sprintf("/market/%d", m.Id) }
hx-replace-url="true" hx-replace-url="true"
hx-target={ fmt.Sprintf("#%s", formId(outcome)) } hx-target={ fmt.Sprintf("#%s", formId(outcome)) }
hx-swap="outerHTML" hx-swap="outerHTML"
hx-select={ fmt.Sprintf("#%s", formId(outcome)) } hx-select={ fmt.Sprintf("#%s", formId(outcome)) }
hx-trigger="input changed delay:1s" hx-trigger="input changed delay:1s"
hx-preserve hx-preserve
hx-disabled-elt="next button" hx-disabled-elt="next button"
hx-indicator={ hxIndicator(outcome) } hx-indicator={ hxIndicator(outcome) }
/> />
<label for="total">you pay:</label> <label for="total">you pay:</label>
<div id="total">{formatPrice(q.TotalPrice)}</div> <div id="total">{ formatPrice(q.TotalPrice) }</div>
<label for="reward">{ "if you win:" }</label> <label for="reward">{ "if you win:" }</label>
<div id="reward">+{formatPrice(q.Reward)}</div> <div id="reward">+{ formatPrice(q.Reward) }</div>
<label for="uQ">you have:</label> <label for="uQ">you have:</label>
<div id="uQ">{ fmt.Sprint(uQ) }</div> <div id="uQ">{ fmt.Sprint(uQ) }</div>
<button type="submit" class="col-span-2">submit</button> <button type="submit" class="col-span-2">submit</button>
</form> </form>
} }
func formId (outcome int) string { func formId(outcome int) string {
return fmt.Sprintf("outcome-%d-form", outcome) return fmt.Sprintf("outcome-%d-form", outcome)
} }
func inputId (outcome int) string { func inputId(outcome int) string {
return fmt.Sprintf("outcome-%d-q", outcome) return fmt.Sprintf("outcome-%d-q", outcome)
} }
func hxIndicator (outcome int) string { func hxIndicator(outcome int) string {
return fmt.Sprintf( return fmt.Sprintf(
"#%s>#p, #%s>#total, #%s>#reward", "#%s>#p, #%s>#total, #%s>#reward",
formId(outcome), formId(outcome), formId(outcome)) formId(outcome), formId(outcome), formId(outcome))
} }
func formatPrice(p float64) string { func formatPrice(p float64) string {

View File

@ -13,8 +13,7 @@ templ Modal(component templ.Component) {
@component @component
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
var $ = selector => document.querySelector(selector) htmx.on("#close", "click", function () {
$("#close").addEventListener("click", function () {
$("#modal").removeAttribute("class") $("#modal").removeAttribute("class")
$("#modal").setAttribute("class", "hidden") $("#modal").setAttribute("class", "hidden")
$("#modal").innerHTML = "" $("#modal").innerHTML = ""

View File

@ -26,13 +26,12 @@ templ Qr(value string, href string) {
templ CopyButton(value string) { templ CopyButton(value string) {
<div class="none" id="copy-data" copy-data={ templ.JSONString(value) } hx-preserve></div> <div class="none" id="copy-data" copy-data={ templ.JSONString(value) } hx-preserve></div>
<script type="text/javascript" id="copy-js" hx-preserve> <script type="text/javascript" id="copy-js" hx-preserve>
var $ = selector => document.querySelector(selector)
var value = JSON.parse($("#copy-data").getAttribute("copy-data")) var value = JSON.parse($("#copy-data").getAttribute("copy-data"))
$("#copy").onclick = function () { htmx.on("#copy", "click", function () {
window.navigator.clipboard.writeText(value) window.navigator.clipboard.writeText(value)
$("#copy").textContent = "copied" $("#copy").textContent = "copied"
setTimeout(() => $("#copy").textContent = "copy", 1000) setTimeout(() => $("#copy").textContent = "copy", 1000)
} })
</script> </script>
} }

View File

@ -1,11 +1,12 @@
package pages package pages
import ( import (
"fmt"
c "git.ekzyis.com/ekzyis/delphi.market/server/router/context" c "git.ekzyis.com/ekzyis/delphi.market/server/router/context"
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components" "git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
"git.ekzyis.com/ekzyis/delphi.market/types" "git.ekzyis.com/ekzyis/delphi.market/types"
"fmt"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"time"
) )
templ Index(markets []types.Market) { templ Index(markets []types.Market) {
@ -30,14 +31,14 @@ templ Index(markets []types.Market) {
</div> </div>
if ctx.Value(c.ReqPathContextKey).(string) == "/" { if ctx.Value(c.ReqPathContextKey).(string) == "/" {
<div class="grid grid-cols-[auto_fit-content(10%)_fit-content(10%)]"> <div class="grid grid-cols-[auto_fit-content(10%)_fit-content(10%)]">
for _, m := range markets { for _, m := range markets {
<span class="ps-3 border-b border-muted pb-3 mt-3"> <span class="ps-3 border-b border-muted pb-3 mt-3">
<a href={ templ.SafeURL(fmt.Sprintf("/market/%d", m.Id)) }>{ m.Question }</a> <a href={ templ.SafeURL(fmt.Sprintf("/market/%d", m.Id)) }>{ m.Question }</a>
<div class="text-small text-muted">{m.User.Name} / {humanize.Time(m.CreatedAt)} / {humanize.Time(m.EndDate)}</div> <div class="text-small text-muted">{ m.User.Name } / { humanize.Time(m.CreatedAt) } / { humanize.Time(m.EndDate) }</div>
</span> </span>
<span class="px-3 border-b border-muted pb-3 mt-3 flex"><div class="self-center">51%</div></span> <span class="px-3 border-b border-muted pb-3 mt-3 flex"><div class="self-center">51%</div></span>
<span class="pe-3 border-b border-muted pb-3 mt-3 flex"><div class="self-center">0</div></span> <span class="pe-3 border-b border-muted pb-3 mt-3 flex"><div class="self-center">0</div></span>
} }
</div> </div>
} else { } else {
<form <form
@ -74,6 +75,7 @@ templ Index(markets []types.Market) {
class="my-1 p-1 text-black" class="my-1 p-1 text-black"
autocomplete="off" autocomplete="off"
required required
min={ minDate() }
/> />
<button type="submit" class="mt-3">submit</button> <button type="submit" class="mt-3">submit</button>
</form> </form>
@ -86,6 +88,10 @@ templ Index(markets []types.Market) {
</html> </html>
} }
func minDate() string {
return time.Now().Add(24 * time.Hour).Format("2006-01-02")
}
func tabStyle(path string, tab string) string { func tabStyle(path string, tab string) string {
class := "!no-underline" class := "!no-underline"
if path == tab { if path == tab {

View File

@ -1,35 +1,35 @@
package pages package pages
import ( import (
"git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components" "git.ekzyis.com/ekzyis/delphi.market/server/router/pages/components"
"git.ekzyis.com/ekzyis/delphi.market/types" "git.ekzyis.com/ekzyis/delphi.market/types"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
) )
// TODO: Add countdown? Use or at least show somewhere precise timestamps? // 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, q0 types.MarketQuote, q1 types.MarketQuote, uQ0 int, uQ1 int) {
<html> <html>
@components.Head() @components.Head()
<body <body
x-data="{ outcome: undefined }" x-data="{ outcome: undefined }"
class="container" class="container"
hx-preserve> hx-preserve
>
@components.Nav() @components.Nav()
<div id="content" class="flex flex-col"> <div id="content" class="flex flex-col">
<small> <small>
@components.Figlet("random", "market") @components.Figlet("random", "market")
</small> </small>
<div class="text-center font-bold my-1">{ m.Question }</div> <div class="text-center font-bold my-1">{ m.Question }</div>
<div class="text-center text-muted my-1">{humanize.Time(m.EndDate)}</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="text-center text-muted my-1"></div>
<blockquote cite="ekzyis" class="p-4 mb-4 border-s-4 border-muted"> <blockquote class="p-4 mb-4 border-s-4 border-muted">
if m.Description != "" { if m.Description != "" {
{ m.Description } { m.Description }
} else { } else {
&lt;empty&gt; &lt;empty&gt;
} }
<div class="text-muted text-right pt-4">― {m.User.Name}, { humanize.Time(m.CreatedAt) }</div> <div class="text-muted text-right pt-4">― { m.User.Name }, { humanize.Time(m.CreatedAt) }</div>
</blockquote> </blockquote>
<div class="flex flex-col justify-center my-1"> <div class="flex flex-col justify-center my-1">
<div class="flex flex-row justify-center"> <div class="flex flex-row justify-center">
@ -55,11 +55,9 @@ templ Market(m types.Market, q0 types.MarketQuote, q1 types.MarketQuote, uQ0 int
@components.MarketForm(m, 0, q0, uQ0) @components.MarketForm(m, 0, q0, uQ0)
</div> </div>
</div> </div>
</div>
</div>
@components.Modal(nil) @components.Modal(nil)
@components.Footer() @components.Footer()
</body> </body>
</html> </html>
} }

View File

@ -29,7 +29,9 @@ templ User(user *types.User) {
<div class="font-bold">joined</div> <div class="font-bold">joined</div>
<div>{ user.CreatedAt.Format(time.DateOnly) }</div> <div>{ user.CreatedAt.Format(time.DateOnly) }</div>
<div class="font-bold">sats</div> <div class="font-bold">sats</div>
<!-- TODO: implement withdrawal of sats -->
<div>{ strconv.Itoa(int(user.Msats) / 1000) }</div> <div>{ strconv.Itoa(int(user.Msats) / 1000) }</div>
<!-- TODO: add WebLN and NWC for send+recv -->
<button hx-post="/logout" class="col-span-2">logout</button> <button hx-post="/logout" class="col-span-2">logout</button>
</div> </div>
</div> </div>