Compare commits
5
Commits
fd403273dd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6c107b936 | ||
|
|
517019d4a4 | ||
|
|
a15d67e210 | ||
|
|
4eaf8ae366 | ||
|
|
21ed051bc2 |
@@ -0,0 +1,237 @@
|
|||||||
|
// Package feedwatch tells the owner when a feed breaks, when it comes back, and
|
||||||
|
// when it goes quiet.
|
||||||
|
//
|
||||||
|
// The poller has always recorded every poll's outcome in source_health, and
|
||||||
|
// /status shows it, but only to someone who goes and looks. Feeds died that way
|
||||||
|
// for months: Siliconera and Digital Foundry had been refused on every poll
|
||||||
|
// thousands of times over before anyone noticed.
|
||||||
|
//
|
||||||
|
// The watcher reads source_health on a timer rather than hooking the pollers, so
|
||||||
|
// that one pass can batch every change into a single message, and so that what
|
||||||
|
// has already been said lives in the database (down_alerted_at,
|
||||||
|
// quiet_alerted_at) instead of in a goroutine that forgets it on restart. Each
|
||||||
|
// episode is reported once: a feed that goes down is announced, stays silent
|
||||||
|
// while it stays down, and is announced again when it comes back.
|
||||||
|
package feedwatch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"pete/internal/config"
|
||||||
|
"pete/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DownAfter is how many failed polls in a row make a feed "down". One or
|
||||||
|
// two failures is weather; five is a feed that has stopped working.
|
||||||
|
DownAfter = 5
|
||||||
|
// QuietAfter is how long a feed can fetch fine and yield nothing new before
|
||||||
|
// it is "quiet". Every healthy source Pete carries produces something within
|
||||||
|
// about four days.
|
||||||
|
QuietAfter = 7 * 24 * time.Hour
|
||||||
|
// Every is how often the watcher looks.
|
||||||
|
Every = 15 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// Report is what one pass found that the owner hasn't been told yet.
|
||||||
|
type Report struct {
|
||||||
|
Down []storage.FeedAlertRow
|
||||||
|
Back []storage.FeedAlertRow
|
||||||
|
Quiet []storage.FeedAlertRow
|
||||||
|
// quietOver are feeds whose quiet spell has ended. They are cleared without
|
||||||
|
// a message: a feed that simply started posting again needs no fanfare.
|
||||||
|
quietOver []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty reports whether there is nothing to say.
|
||||||
|
func (r Report) Empty() bool { return len(r.Down)+len(r.Back)+len(r.Quiet) == 0 }
|
||||||
|
|
||||||
|
// Check reads source_health and works out what has changed, without recording
|
||||||
|
// anything. Only sources that are enabled in the config are considered, so a
|
||||||
|
// feed that has been switched off or removed never alerts.
|
||||||
|
func Check(now time.Time, sources []config.SourceConfig) (Report, error) {
|
||||||
|
enabled := make(map[string]bool, len(sources))
|
||||||
|
for _, s := range sources {
|
||||||
|
if s.Enabled {
|
||||||
|
enabled[s.Name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows, err := storage.ListFeedAlertRows()
|
||||||
|
if err != nil {
|
||||||
|
return Report{}, err
|
||||||
|
}
|
||||||
|
var r Report
|
||||||
|
quietCutoff := now.Add(-QuietAfter).Unix()
|
||||||
|
for _, row := range rows {
|
||||||
|
if !enabled[row.Source] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
down := row.ConsecutiveFailures >= DownAfter
|
||||||
|
switch {
|
||||||
|
case down && row.DownAlertedAt == 0:
|
||||||
|
r.Down = append(r.Down, row)
|
||||||
|
// Back means "has fetched since we said it was down", not "is fetching
|
||||||
|
// right now": a feed that recovers and breaks again between two passes
|
||||||
|
// still gets its all-clear, and then a fresh down alert next pass.
|
||||||
|
case row.DownAlertedAt != 0 && row.LastSuccessAt > row.DownAlertedAt:
|
||||||
|
r.Back = append(r.Back, row)
|
||||||
|
}
|
||||||
|
// A feed that isn't fetching can't be judged on whether it has news;
|
||||||
|
// "down" already covers it.
|
||||||
|
if row.ConsecutiveFailures > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
quiet := row.LastNewAt > 0 && row.LastNewAt < quietCutoff
|
||||||
|
switch {
|
||||||
|
case quiet && row.QuietAlertedAt == 0:
|
||||||
|
r.Quiet = append(r.Quiet, row)
|
||||||
|
case !quiet && row.QuietAlertedAt != 0:
|
||||||
|
r.quietOver = append(r.quietOver, row.Source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit records that the owner has now been told everything in r.
|
||||||
|
func Commit(now time.Time, r Report) error {
|
||||||
|
at := now.Unix()
|
||||||
|
for _, row := range r.Down {
|
||||||
|
if err := storage.SetFeedDownAlerted(row.Source, at); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, row := range r.Back {
|
||||||
|
if err := storage.SetFeedDownAlerted(row.Source, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, row := range r.Quiet {
|
||||||
|
if err := storage.SetFeedQuietAlerted(row.Source, at); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, src := range r.quietOver {
|
||||||
|
if err := storage.SetFeedQuietAlerted(src, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message renders r as the plain-text note Pete sends the owner.
|
||||||
|
func Message(now time.Time, r Report) string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("Feed check")
|
||||||
|
if len(r.Down) > 0 {
|
||||||
|
b.WriteString("\n\nNot fetching:")
|
||||||
|
for _, row := range r.Down {
|
||||||
|
since := "has never fetched successfully"
|
||||||
|
if row.LastSuccessAt > 0 {
|
||||||
|
since = "last worked " + ago(now, row.LastSuccessAt)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&b, "\n• %s: %s failed polls in a row, %s. Last error: %s",
|
||||||
|
row.Source, count(row.ConsecutiveFailures), since, short(row.LastError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(r.Back) > 0 {
|
||||||
|
b.WriteString("\n\nWorking again:")
|
||||||
|
for _, row := range r.Back {
|
||||||
|
fmt.Fprintf(&b, "\n• %s is fetching again.", row.Source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(r.Quiet) > 0 {
|
||||||
|
b.WriteString("\n\nGone quiet:")
|
||||||
|
for _, row := range r.Quiet {
|
||||||
|
fmt.Fprintf(&b, "\n• %s fetches fine but hasn't had anything new in %s.",
|
||||||
|
row.Source, span(now, row.LastNewAt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send delivers a message to the owner. An error means it didn't arrive, and
|
||||||
|
// the watcher will try again next pass rather than mark it said.
|
||||||
|
type Send func(ctx context.Context, msg string) error
|
||||||
|
|
||||||
|
// Run checks once shortly after startup (so pollers have had a first go) and
|
||||||
|
// then every Every until ctx ends.
|
||||||
|
func Run(ctx context.Context, sources []config.SourceConfig, send Send) {
|
||||||
|
first := time.NewTimer(2 * time.Minute)
|
||||||
|
defer first.Stop()
|
||||||
|
t := time.NewTicker(Every)
|
||||||
|
defer t.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-first.C:
|
||||||
|
case <-t.C:
|
||||||
|
}
|
||||||
|
pass(ctx, time.Now(), sources, send)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pass(ctx context.Context, now time.Time, sources []config.SourceConfig, send Send) {
|
||||||
|
r, err := Check(now, sources)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("feedwatch: check failed", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Empty() {
|
||||||
|
// Still commit: a quiet spell may have ended silently.
|
||||||
|
if err := Commit(now, r); err != nil {
|
||||||
|
slog.Error("feedwatch: commit failed", "err", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := Message(now, r)
|
||||||
|
if err := send(ctx, msg); err != nil {
|
||||||
|
slog.Error("feedwatch: alert not delivered, will retry", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slog.Info("feedwatch: alert sent", "down", len(r.Down), "back", len(r.Back), "quiet", len(r.Quiet))
|
||||||
|
if err := Commit(now, r); err != nil {
|
||||||
|
slog.Error("feedwatch: commit failed", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// span renders the time since unix as a rough human length ("9 days").
|
||||||
|
func span(now time.Time, unix int64) string {
|
||||||
|
d := now.Sub(time.Unix(unix, 0))
|
||||||
|
switch {
|
||||||
|
case d < 2*time.Hour:
|
||||||
|
return fmt.Sprintf("%d minutes", int(d.Minutes()))
|
||||||
|
case d < 48*time.Hour:
|
||||||
|
return fmt.Sprintf("%d hours", int(d.Hours()))
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d days", int(d.Hours()/24))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ago is span as a point in the past ("9 days ago").
|
||||||
|
func ago(now time.Time, unix int64) string { return span(now, unix) + " ago" }
|
||||||
|
|
||||||
|
// count renders n with thousands separators.
|
||||||
|
func count(n int) string {
|
||||||
|
s := fmt.Sprint(n)
|
||||||
|
for i := len(s) - 3; i > 0; i -= 3 {
|
||||||
|
s = s[:i] + "," + s[i:]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// short trims an error to one readable line.
|
||||||
|
func short(s string) string {
|
||||||
|
s = strings.TrimSpace(strings.ReplaceAll(s, "\n", " "))
|
||||||
|
if s == "" {
|
||||||
|
return "none recorded"
|
||||||
|
}
|
||||||
|
if len(s) > 140 {
|
||||||
|
s = s[:140] + "…"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package feedwatch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"pete/internal/config"
|
||||||
|
"pete/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setup(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
storage.Close()
|
||||||
|
if err := storage.Init(filepath.Join(t.TempDir(), "feedwatch.db")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { storage.Close() })
|
||||||
|
}
|
||||||
|
|
||||||
|
func src(names ...string) []config.SourceConfig {
|
||||||
|
var out []config.SourceConfig
|
||||||
|
for _, n := range names {
|
||||||
|
out = append(out, config.SourceConfig{Name: n, Enabled: true})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func fail(name string, n int) {
|
||||||
|
for range n {
|
||||||
|
storage.RecordPollResult(name, false, 0, errors.New("feed fetch https://x/feed: status 403"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setLastNew backdates a source's last new story.
|
||||||
|
func setLastNew(t *testing.T, name string, at time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
if _, err := storage.Get().Exec(`UPDATE source_health SET last_new_at = ? WHERE source = ?`, at.Unix(), name); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// capture is a Send that records what it was asked to deliver.
|
||||||
|
type capture struct {
|
||||||
|
msgs []string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *capture) send(_ context.Context, msg string) error {
|
||||||
|
if c.err != nil {
|
||||||
|
return c.err
|
||||||
|
}
|
||||||
|
c.msgs = append(c.msgs, msg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownIsReportedOnceAndBackIsReported(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
sources := src("Broken", "Fine")
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
storage.RecordPollResult("Fine", true, 10, nil)
|
||||||
|
fail("Broken", DownAfter-1)
|
||||||
|
|
||||||
|
c := &capture{}
|
||||||
|
pass(context.Background(), now, sources, c.send)
|
||||||
|
if len(c.msgs) != 0 {
|
||||||
|
t.Fatalf("alerted below the threshold: %q", c.msgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fail("Broken", 1)
|
||||||
|
pass(context.Background(), now, sources, c.send)
|
||||||
|
if len(c.msgs) != 1 || !strings.Contains(c.msgs[0], "Broken: 5 failed polls in a row, has never fetched successfully") {
|
||||||
|
t.Fatalf("down alert = %q", c.msgs)
|
||||||
|
}
|
||||||
|
if strings.Contains(c.msgs[0], "Fine") {
|
||||||
|
t.Errorf("a healthy feed was mentioned: %q", c.msgs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still down on the next pass: nothing new to say.
|
||||||
|
fail("Broken", 3)
|
||||||
|
pass(context.Background(), now.Add(Every), sources, c.send)
|
||||||
|
if len(c.msgs) != 1 {
|
||||||
|
t.Fatalf("down alert repeated: %q", c.msgs[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// It comes back. The success has to be stamped after the alert, so step
|
||||||
|
// past the second the alert was recorded in.
|
||||||
|
time.Sleep(1100 * time.Millisecond)
|
||||||
|
storage.RecordPollResult("Broken", true, 5, nil)
|
||||||
|
pass(context.Background(), time.Now(), sources, c.send)
|
||||||
|
if len(c.msgs) != 2 || !strings.Contains(c.msgs[1], "Broken is fetching again") {
|
||||||
|
t.Fatalf("recovery = %q", c.msgs)
|
||||||
|
}
|
||||||
|
pass(context.Background(), time.Now(), sources, c.send)
|
||||||
|
if len(c.msgs) != 2 {
|
||||||
|
t.Fatalf("recovery repeated: %q", c.msgs[2:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUndeliveredAlertIsRetried(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
sources := src("Broken")
|
||||||
|
fail("Broken", DownAfter)
|
||||||
|
|
||||||
|
c := &capture{err: errors.New("homeserver unreachable")}
|
||||||
|
pass(context.Background(), time.Now(), sources, c.send)
|
||||||
|
|
||||||
|
c.err = nil
|
||||||
|
pass(context.Background(), time.Now(), sources, c.send)
|
||||||
|
if len(c.msgs) != 1 || !strings.Contains(c.msgs[0], "Broken") {
|
||||||
|
t.Fatalf("alert after a failed send = %q, want it delivered on the retry", c.msgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuietFeedIsReportedOnce(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
sources := src("Sleepy", "Busy")
|
||||||
|
now := time.Now()
|
||||||
|
storage.RecordPollResult("Sleepy", true, 10, nil)
|
||||||
|
storage.RecordPollResult("Busy", true, 10, nil)
|
||||||
|
setLastNew(t, "Sleepy", now.Add(-9*24*time.Hour))
|
||||||
|
|
||||||
|
c := &capture{}
|
||||||
|
pass(context.Background(), now, sources, c.send)
|
||||||
|
if len(c.msgs) != 1 || !strings.Contains(c.msgs[0], "Sleepy fetches fine but hasn't had anything new in 9 days") {
|
||||||
|
t.Fatalf("quiet alert = %q", c.msgs)
|
||||||
|
}
|
||||||
|
if strings.Contains(c.msgs[0], "Busy") {
|
||||||
|
t.Errorf("a busy feed was called quiet: %q", c.msgs[0])
|
||||||
|
}
|
||||||
|
pass(context.Background(), now.Add(Every), sources, c.send)
|
||||||
|
if len(c.msgs) != 1 {
|
||||||
|
t.Fatalf("quiet alert repeated: %q", c.msgs[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// New stories end the quiet spell without a message, and a later quiet
|
||||||
|
// spell is a new episode that gets reported again.
|
||||||
|
storage.RecordNewStories("Sleepy", 2)
|
||||||
|
pass(context.Background(), now, sources, c.send)
|
||||||
|
if len(c.msgs) != 1 {
|
||||||
|
t.Fatalf("end of a quiet spell was announced: %q", c.msgs[1:])
|
||||||
|
}
|
||||||
|
setLastNew(t, "Sleepy", now.Add(-8*24*time.Hour))
|
||||||
|
pass(context.Background(), now, sources, c.send)
|
||||||
|
if len(c.msgs) != 2 {
|
||||||
|
t.Fatalf("second quiet spell not reported: %q", c.msgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A feed that has just been added has no history, so it must not be called
|
||||||
|
// quiet before it has had a full window to produce something.
|
||||||
|
func TestNewSourceGetsAGracePeriod(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
storage.RecordPollResult("Fresh", true, 0, nil)
|
||||||
|
r, err := Check(time.Now().Add(QuietAfter-time.Hour), src("Fresh"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !r.Empty() {
|
||||||
|
t.Fatalf("new source reported inside its grace period: %+v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisabledSourcesNeverAlert(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
fail("Retired", DownAfter*2)
|
||||||
|
sources := []config.SourceConfig{{Name: "Retired", Enabled: false}}
|
||||||
|
r, err := Check(time.Now(), sources)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !r.Empty() {
|
||||||
|
t.Fatalf("disabled source alerted: %+v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -118,6 +118,16 @@ func (p *Poller) pollOnceWithErr(ctx context.Context, src config.SourceConfig) e
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An item older than the retention window is one Pete has either
|
||||||
|
// already shown and since pruned, or never needs to: maintenance would
|
||||||
|
// delete it on the next pass. Without this, a slow feed that still
|
||||||
|
// lists month-old items re-ingests them as new the day after their row
|
||||||
|
// is pruned, since that row was the only memory of them. Items with no
|
||||||
|
// date can't be judged and pass through.
|
||||||
|
if items[i].PublishedAt > 0 && time.Since(time.Unix(items[i].PublishedAt, 0)) > storage.StoryRetention {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Drop items in languages this source isn't configured to surface.
|
// Drop items in languages this source isn't configured to surface.
|
||||||
// Items without a language tag pass through — only filter when the
|
// Items without a language tag pass through — only filter when the
|
||||||
// feed explicitly marks an item with a non-matching language.
|
// feed explicitly marks an item with a non-matching language.
|
||||||
@@ -255,6 +265,7 @@ func (p *Poller) pollOnceWithErr(ctx context.Context, src config.SourceConfig) e
|
|||||||
|
|
||||||
if newCount > 0 {
|
if newCount > 0 {
|
||||||
slog.Info("ingested new stories", "source", src.Name, "count", newCount)
|
slog.Info("ingested new stories", "source", src.Name, "count", newCount)
|
||||||
|
storage.RecordNewStories(src.Name, newCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+13
-10
@@ -508,22 +508,25 @@ func (c *Client) sendImage(ctx context.Context, roomID id.RoomID, imageURL strin
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendAdminWarning sends a warning message to the admin room, if configured.
|
// SendAdminNotice sends a notice to the admin room. It returns an error when no
|
||||||
func (c *Client) SendAdminWarning(msg string) {
|
// admin room is configured or the send fails, so a caller that must not lose
|
||||||
|
// the message (the feed watcher) can try again later.
|
||||||
|
func (c *Client) SendAdminNotice(ctx context.Context, msg string) error {
|
||||||
if c.adminRoom == nil {
|
if c.adminRoom == nil {
|
||||||
slog.Warn("admin warning (no admin room configured)", "msg", msg)
|
return fmt.Errorf("no admin room configured")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
content := &event.MessageEventContent{
|
content := &event.MessageEventContent{
|
||||||
MsgType: event.MsgText,
|
MsgType: event.MsgNotice,
|
||||||
Body: msg,
|
Body: msg,
|
||||||
}
|
}
|
||||||
|
if _, err := c.mx.SendMessageEvent(ctx, *c.adminRoom, event.EventMessage, content); err != nil {
|
||||||
|
return fmt.Errorf("send admin notice: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := c.mx.SendMessageEvent(context.Background(), *c.adminRoom, event.EventMessage, content); err != nil {
|
// HasAdminRoom reports whether an admin room is configured.
|
||||||
slog.Error("failed to send admin warning", "err", err, "msg", msg)
|
func (c *Client) HasAdminRoom() bool { return c.adminRoom != nil }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChannelRoomID returns the room ID for a named channel.
|
// ChannelRoomID returns the room ID for a named channel.
|
||||||
// Returns false if the channel is not configured or has an empty room ID.
|
// Returns false if the channel is not configured or has an empty room ID.
|
||||||
|
|||||||
+43
-4
@@ -1,6 +1,7 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -8,6 +9,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
@@ -94,6 +96,14 @@ func runMigrations(d *sql.DB) error {
|
|||||||
addColumnIfMissing(d, "post_log", "url_canonical", "TEXT")
|
addColumnIfMissing(d, "post_log", "url_canonical", "TEXT")
|
||||||
addColumnIfMissing(d, "post_log", "forced", "INTEGER NOT NULL DEFAULT 0")
|
addColumnIfMissing(d, "post_log", "forced", "INTEGER NOT NULL DEFAULT 0")
|
||||||
addColumnIfMissing(d, "round_robin_state", "last_channel", "TEXT")
|
addColumnIfMissing(d, "round_robin_state", "last_channel", "TEXT")
|
||||||
|
// Feed alerts. last_new_at is when a source last produced a story Pete kept;
|
||||||
|
// the two *_alerted_at stamps remember that the owner has already been told a
|
||||||
|
// feed is down or has gone quiet, so an alert fires once per episode and
|
||||||
|
// survives a restart. See backfillLastNewAt for what existing rows start with.
|
||||||
|
addColumnIfMissing(d, "source_health", "last_new_at", "INTEGER")
|
||||||
|
addColumnIfMissing(d, "source_health", "down_alerted_at", "INTEGER")
|
||||||
|
addColumnIfMissing(d, "source_health", "quiet_alerted_at", "INTEGER")
|
||||||
|
backfillLastNewAt(d)
|
||||||
// Occupancy of a shared table. Rows written before the casino went multiplayer
|
// Occupancy of a shared table. Rows written before the casino went multiplayer
|
||||||
// are solo games and read as NULL, which is exactly what they are.
|
// are solo games and read as NULL, which is exactly what they are.
|
||||||
addColumnIfMissing(d, "game_live_hands", "table_id", "TEXT")
|
addColumnIfMissing(d, "game_live_hands", "table_id", "TEXT")
|
||||||
@@ -154,12 +164,41 @@ func runMigrations(d *sql.DB) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunMaintenance prunes stale data. Called periodically.
|
// StoryRetention is how long a story is kept after Pete first saw it. It is also
|
||||||
|
// how long Pete remembers having seen it: the stories row is the only record of
|
||||||
|
// its GUID, canonical URL and headline, so the poller ignores feed items older
|
||||||
|
// than this rather than meet them again as strangers once their row is gone.
|
||||||
|
const StoryRetention = 30 * 24 * time.Hour
|
||||||
|
|
||||||
|
// StartMaintenance runs RunMaintenance now and then once a day until ctx ends.
|
||||||
|
// It used to run only at startup, so on a service that went weeks between
|
||||||
|
// deploys nothing was pruned, and then everything was, all at once.
|
||||||
|
func StartMaintenance(ctx context.Context) {
|
||||||
|
RunMaintenance()
|
||||||
|
go func() {
|
||||||
|
t := time.NewTicker(24 * time.Hour)
|
||||||
|
defer t.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
RunMaintenance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunMaintenance prunes stale data.
|
||||||
func RunMaintenance() {
|
func RunMaintenance() {
|
||||||
// Prune old stories (30 days) and their post logs / reactions
|
// Prune old stories and their post logs / reactions. A story someone has
|
||||||
storyCutoff := nowUnix() - int64(30*86400)
|
// bookmarked is kept however old it is: the orphan sweep below would
|
||||||
|
// otherwise take the bookmark with it, and a saved story that quietly
|
||||||
|
// vanishes after a month is exactly what saving it was meant to prevent.
|
||||||
|
storyCutoff := nowUnix() - int64(StoryRetention/time.Second)
|
||||||
exec("prune old stories",
|
exec("prune old stories",
|
||||||
`DELETE FROM stories WHERE seen_at < ? AND classified = 1`, storyCutoff)
|
`DELETE FROM stories WHERE seen_at < ? AND classified = 1
|
||||||
|
AND id NOT IN (SELECT story_id FROM user_story_state WHERE bookmarked_at IS NOT NULL)`, storyCutoff)
|
||||||
exec("prune old post_log",
|
exec("prune old post_log",
|
||||||
`DELETE FROM post_log WHERE posted_at < ?`, storyCutoff)
|
`DELETE FROM post_log WHERE posted_at < ?`, storyCutoff)
|
||||||
exec("prune old reactions",
|
exec("prune old reactions",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package storage
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SourceHealth is one source's persisted poll health, as written by the poller.
|
// SourceHealth is one source's persisted poll health, as written by the poller.
|
||||||
@@ -30,15 +31,17 @@ type SourceContentStat struct {
|
|||||||
// RecordPollResult upserts a source's health row after a poll attempt. On
|
// RecordPollResult upserts a source's health row after a poll attempt. On
|
||||||
// success it clears the error and failure counter and records the item count;
|
// success it clears the error and failure counter and records the item count;
|
||||||
// on failure it bumps consecutive_failures and stores the message while
|
// on failure it bumps consecutive_failures and stores the message while
|
||||||
// preserving the last successful timestamp and item count. Fire-and-forget:
|
// preserving the last successful timestamp and item count. A brand-new row
|
||||||
|
// starts its last_new_at clock at the first poll, so a just-added source gets a
|
||||||
|
// full quiet window before it can be called quiet. Fire-and-forget:
|
||||||
// a failure here must never disrupt polling, so errors are logged and swallowed.
|
// a failure here must never disrupt polling, so errors are logged and swallowed.
|
||||||
func RecordPollResult(source string, ok bool, itemCount int, pollErr error) {
|
func RecordPollResult(source string, ok bool, itemCount int, pollErr error) {
|
||||||
now := nowUnix()
|
now := nowUnix()
|
||||||
if ok {
|
if ok {
|
||||||
exec("record poll success",
|
exec("record poll success",
|
||||||
`INSERT INTO source_health
|
`INSERT INTO source_health
|
||||||
(source, last_poll_at, last_success_at, last_error, consecutive_failures, last_item_count, updated_at)
|
(source, last_poll_at, last_success_at, last_error, consecutive_failures, last_item_count, updated_at, last_new_at)
|
||||||
VALUES (?, ?, ?, '', 0, ?, ?)
|
VALUES (?, ?, ?, '', 0, ?, ?, ?)
|
||||||
ON CONFLICT(source) DO UPDATE SET
|
ON CONFLICT(source) DO UPDATE SET
|
||||||
last_poll_at = excluded.last_poll_at,
|
last_poll_at = excluded.last_poll_at,
|
||||||
last_success_at = excluded.last_success_at,
|
last_success_at = excluded.last_success_at,
|
||||||
@@ -46,7 +49,7 @@ func RecordPollResult(source string, ok bool, itemCount int, pollErr error) {
|
|||||||
consecutive_failures = 0,
|
consecutive_failures = 0,
|
||||||
last_item_count = excluded.last_item_count,
|
last_item_count = excluded.last_item_count,
|
||||||
updated_at = excluded.updated_at`,
|
updated_at = excluded.updated_at`,
|
||||||
source, now, now, itemCount, now)
|
source, now, now, itemCount, now, now)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msg := ""
|
msg := ""
|
||||||
@@ -55,14 +58,14 @@ func RecordPollResult(source string, ok bool, itemCount int, pollErr error) {
|
|||||||
}
|
}
|
||||||
exec("record poll failure",
|
exec("record poll failure",
|
||||||
`INSERT INTO source_health
|
`INSERT INTO source_health
|
||||||
(source, last_poll_at, last_success_at, last_error, consecutive_failures, last_item_count, updated_at)
|
(source, last_poll_at, last_success_at, last_error, consecutive_failures, last_item_count, updated_at, last_new_at)
|
||||||
VALUES (?, ?, NULL, ?, 1, 0, ?)
|
VALUES (?, ?, NULL, ?, 1, 0, ?, ?)
|
||||||
ON CONFLICT(source) DO UPDATE SET
|
ON CONFLICT(source) DO UPDATE SET
|
||||||
last_poll_at = excluded.last_poll_at,
|
last_poll_at = excluded.last_poll_at,
|
||||||
last_error = excluded.last_error,
|
last_error = excluded.last_error,
|
||||||
consecutive_failures = source_health.consecutive_failures + 1,
|
consecutive_failures = source_health.consecutive_failures + 1,
|
||||||
updated_at = excluded.updated_at`,
|
updated_at = excluded.updated_at`,
|
||||||
source, now, msg, now)
|
source, now, msg, now, now)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListSourceHealth returns the poll-health row for every source that has been
|
// ListSourceHealth returns the poll-health row for every source that has been
|
||||||
@@ -147,3 +150,79 @@ func SourceContentStats() (map[string]SourceContentStat, error) {
|
|||||||
}
|
}
|
||||||
return out, prows.Err()
|
return out, prows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// backfillLastNewAt gives rows that predate last_new_at a starting value: the
|
||||||
|
// newest story still retained for that source, or, when none is, the time of
|
||||||
|
// the migration. The second case is a grace period rather than a guess: a
|
||||||
|
// source with no retained stories might be long dead or might have been added
|
||||||
|
// yesterday, and nothing on disk says which, so it gets a full quiet window
|
||||||
|
// before it can be called quiet.
|
||||||
|
func backfillLastNewAt(d *sql.DB) {
|
||||||
|
if _, err := d.Exec(
|
||||||
|
`UPDATE source_health
|
||||||
|
SET last_new_at = COALESCE(
|
||||||
|
(SELECT MAX(seen_at) FROM stories WHERE stories.source = source_health.source),
|
||||||
|
?)
|
||||||
|
WHERE last_new_at IS NULL`, nowUnix()); err != nil {
|
||||||
|
slog.Error("backfill source_health.last_new_at failed", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordNewStories notes that a poll of source kept n genuinely new stories.
|
||||||
|
// Only a non-zero n moves last_new_at: a poll that fetched fine but found
|
||||||
|
// nothing new is exactly what "gone quiet" is made of.
|
||||||
|
func RecordNewStories(source string, n int) {
|
||||||
|
if n <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exec("record new stories",
|
||||||
|
`UPDATE source_health SET last_new_at = ? WHERE source = ?`, nowUnix(), source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeedAlertRow is the slice of source_health the feed watcher reads.
|
||||||
|
type FeedAlertRow struct {
|
||||||
|
Source string
|
||||||
|
ConsecutiveFailures int
|
||||||
|
LastError string
|
||||||
|
LastSuccessAt int64 // 0 = never fetched successfully
|
||||||
|
LastNewAt int64 // 0 = unknown
|
||||||
|
DownAlertedAt int64 // 0 = owner not told it's down
|
||||||
|
QuietAlertedAt int64 // 0 = owner not told it's quiet
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFeedAlertRows returns the alert-relevant health of every polled source.
|
||||||
|
func ListFeedAlertRows() ([]FeedAlertRow, error) {
|
||||||
|
rows, err := Get().Query(
|
||||||
|
`SELECT source, consecutive_failures, COALESCE(last_error, ''),
|
||||||
|
COALESCE(last_success_at, 0), COALESCE(last_new_at, 0),
|
||||||
|
COALESCE(down_alerted_at, 0), COALESCE(quiet_alerted_at, 0)
|
||||||
|
FROM source_health ORDER BY source`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list feed alert rows: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []FeedAlertRow
|
||||||
|
for rows.Next() {
|
||||||
|
var r FeedAlertRow
|
||||||
|
if err := rows.Scan(&r.Source, &r.ConsecutiveFailures, &r.LastError,
|
||||||
|
&r.LastSuccessAt, &r.LastNewAt, &r.DownAlertedAt, &r.QuietAlertedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, r)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFeedDownAlerted records (at > 0) or clears (at == 0) that the owner has
|
||||||
|
// been told source is down.
|
||||||
|
func SetFeedDownAlerted(source string, at int64) error {
|
||||||
|
_, err := Get().Exec(`UPDATE source_health SET down_alerted_at = NULLIF(?, 0) WHERE source = ?`, at, source)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFeedQuietAlerted records (at > 0) or clears (at == 0) that the owner has
|
||||||
|
// been told source has gone quiet.
|
||||||
|
func SetFeedQuietAlerted(source string, at int64) error {
|
||||||
|
_, err := Get().Exec(`UPDATE source_health SET quiet_alerted_at = NULLIF(?, 0) WHERE source = ?`, at, source)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -124,3 +124,46 @@ func TestUserStoryState_ScopedBySub(t *testing.T) {
|
|||||||
t.Fatalf("user-b count = %d, want 0", n)
|
t.Fatalf("user-b count = %d, want 0", n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Maintenance prunes stories older than the retention window, and then sweeps
|
||||||
|
// away user rows pointing at them. A bookmark is someone asking for a story to
|
||||||
|
// be kept, so a bookmarked story must survive the prune, and its bookmark with
|
||||||
|
// it. Read markers on pruned stories are fair game.
|
||||||
|
func TestMaintenanceKeepsBookmarkedStories(t *testing.T) {
|
||||||
|
setupTestDB(t)
|
||||||
|
saved := seedStory(t, "saved") // seedStory stamps 2023, far past retention
|
||||||
|
readOnly := seedStory(t, "read") // read, not saved
|
||||||
|
seedStory(t, "untouched")
|
||||||
|
const sub = "ak-sub-keep"
|
||||||
|
|
||||||
|
if err := SetBookmark(sub, saved, true); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := SetRead(sub, readOnly, true); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
RunMaintenance()
|
||||||
|
|
||||||
|
var n int
|
||||||
|
if err := Get().QueryRow(`SELECT COUNT(*) FROM stories`).Scan(&n); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if n != 1 {
|
||||||
|
t.Errorf("stories left after maintenance = %d, want 1 (the bookmarked one)", n)
|
||||||
|
}
|
||||||
|
marks, err := ListBookmarks(sub, 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(marks) != 1 || marks[0].GUID != "saved" {
|
||||||
|
t.Fatalf("bookmarks after maintenance = %+v, want just the saved story", marks)
|
||||||
|
}
|
||||||
|
read, _, err := UserStoryState(sub, []int64{readOnly})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if read[readOnly] {
|
||||||
|
t.Error("read marker on a pruned story survived the orphan sweep")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Static assets are served out of the embedded FS, and embedded files have no
|
||||||
|
// modification time, so http.FileServer sends no Last-Modified and no ETag. With
|
||||||
|
// no validator and no Cache-Control, every page view re-downloaded the whole
|
||||||
|
// stylesheet and every script in full.
|
||||||
|
//
|
||||||
|
// So each file is hashed once, at first use, and the hash does two jobs:
|
||||||
|
//
|
||||||
|
// - Templates link assets through {{asset "/static/..."}}, which appends
|
||||||
|
// ?v=<hash>. A request carrying the current hash is content-addressed, so it
|
||||||
|
// is served immutable for a year: the next deploy that changes the file
|
||||||
|
// changes the URL, and the browser never has to ask.
|
||||||
|
// - Every static response carries the hash as its ETag. Anything fetched by a
|
||||||
|
// bare URL (audio samples, scripts loading scripts, a stale page asking for
|
||||||
|
// an old ?v=) is served no-cache, and the revalidation is a cheap 304.
|
||||||
|
|
||||||
|
var (
|
||||||
|
assetOnce sync.Once
|
||||||
|
assetHashes map[string]string // path under static/ -> short content hash
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadAssetHashes() map[string]string {
|
||||||
|
assetOnce.Do(func() {
|
||||||
|
assetHashes = map[string]string{}
|
||||||
|
sub, err := fs.Sub(staticFS, "static")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = fs.WalkDir(sub, ".", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil || d.IsDir() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b, err := fs.ReadFile(sub, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sum := sha256.Sum256(b)
|
||||||
|
assetHashes[path] = hex.EncodeToString(sum[:])[:12]
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return assetHashes
|
||||||
|
}
|
||||||
|
|
||||||
|
// assetURL is the {{asset}} template func: "/static/css/output.css" becomes
|
||||||
|
// "/static/css/output.css?v=<hash>". A path that isn't an embedded file comes
|
||||||
|
// back unchanged rather than breaking the page.
|
||||||
|
func assetURL(path string) string {
|
||||||
|
h, ok := loadAssetHashes()[strings.TrimPrefix(path, "/static/")]
|
||||||
|
if !ok {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return path + "?v=" + h
|
||||||
|
}
|
||||||
|
|
||||||
|
// staticHandler serves the embedded static tree (mounted under /static/ with the
|
||||||
|
// prefix already stripped) with a content-hash ETag and a cache policy that
|
||||||
|
// depends on whether the URL named the current hash.
|
||||||
|
func staticHandler(sub fs.FS) http.Handler {
|
||||||
|
files := http.FileServer(http.FS(sub))
|
||||||
|
hashes := loadAssetHashes()
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h, ok := hashes[strings.TrimPrefix(r.URL.Path, "/")]; ok {
|
||||||
|
// FileServer honours If-None-Match against an ETag already set on
|
||||||
|
// the response, so this alone turns revalidation into a 304.
|
||||||
|
w.Header().Set("ETag", `"`+h+`"`)
|
||||||
|
if r.URL.Query().Get("v") == h {
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func serveStatic(t *testing.T, target string, hdr map[string]string) *httptest.ResponseRecorder {
|
||||||
|
t.Helper()
|
||||||
|
sub, err := fs.Sub(staticFS, "static")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
h := http.StripPrefix("/static/", staticHandler(sub))
|
||||||
|
req := httptest.NewRequest("GET", target, nil)
|
||||||
|
for k, v := range hdr {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, req)
|
||||||
|
return rec
|
||||||
|
}
|
||||||
|
|
||||||
|
// The whole point: a page's stylesheet link is content-addressed and cached for
|
||||||
|
// good, a bare URL revalidates, and revalidation costs a 304 rather than 90KB.
|
||||||
|
func TestStaticAssetsAreCacheable(t *testing.T) {
|
||||||
|
versioned := assetURL("/static/css/output.css")
|
||||||
|
if !strings.Contains(versioned, "?v=") {
|
||||||
|
t.Fatalf("assetURL did not version the stylesheet: %q", versioned)
|
||||||
|
}
|
||||||
|
|
||||||
|
rec := serveStatic(t, versioned, nil)
|
||||||
|
if rec.Code != 200 {
|
||||||
|
t.Fatalf("versioned GET = %d, want 200", rec.Code)
|
||||||
|
}
|
||||||
|
if cc := rec.Header().Get("Cache-Control"); !strings.Contains(cc, "immutable") {
|
||||||
|
t.Errorf("versioned Cache-Control = %q, want immutable", cc)
|
||||||
|
}
|
||||||
|
etag := rec.Header().Get("ETag")
|
||||||
|
if etag == "" {
|
||||||
|
t.Fatal("no ETag on a static asset")
|
||||||
|
}
|
||||||
|
|
||||||
|
rec = serveStatic(t, "/static/css/output.css", nil)
|
||||||
|
if cc := rec.Header().Get("Cache-Control"); cc != "no-cache" {
|
||||||
|
t.Errorf("bare Cache-Control = %q, want no-cache", cc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A stale page asking for a hash that no longer exists gets today's bytes,
|
||||||
|
// but must not be told to keep them for a year under the old name.
|
||||||
|
rec = serveStatic(t, "/static/css/output.css?v=000000000000", nil)
|
||||||
|
if cc := rec.Header().Get("Cache-Control"); cc != "no-cache" {
|
||||||
|
t.Errorf("stale-hash Cache-Control = %q, want no-cache", cc)
|
||||||
|
}
|
||||||
|
|
||||||
|
rec = serveStatic(t, "/static/css/output.css", map[string]string{"If-None-Match": etag})
|
||||||
|
if rec.Code != http.StatusNotModified {
|
||||||
|
t.Errorf("revalidation = %d, want 304", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAssetURLLeavesUnknownPathsAlone(t *testing.T) {
|
||||||
|
if got := assetURL("/static/nope.js"); got != "/static/nope.js" {
|
||||||
|
t.Errorf("assetURL(unknown) = %q, want it unchanged", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"pete/internal/config"
|
||||||
|
"pete/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A sibling parodia.dev host is the same *site* as Pete, so SameSite=Lax lets
|
||||||
|
// the session cookie ride along on its forms. The server has to refuse those
|
||||||
|
// itself, and must still let a same-origin page and a headerless server-to-server
|
||||||
|
// call (gogobee, with its bearer token) through.
|
||||||
|
func TestCrossOriginWritesAreRefused(t *testing.T) {
|
||||||
|
storage.Close()
|
||||||
|
if err := storage.Init(filepath.Join(t.TempDir(), "csrf.db")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { storage.Close() })
|
||||||
|
|
||||||
|
s, err := New(config.WebConfig{SiteTitle: "Pete", ListenAddr: ":0"}, nil, true, config.AdventureConfig{}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
post := func(hdr map[string]string) int {
|
||||||
|
req := httptest.NewRequest("POST", "https://news.parodia.dev/api/state/read", nil)
|
||||||
|
for k, v := range hdr {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
s.srv.Handler.ServeHTTP(rec, req)
|
||||||
|
return rec.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
if code := post(map[string]string{"Sec-Fetch-Site": "cross-site"}); code != http.StatusForbidden {
|
||||||
|
t.Errorf("cross-site POST = %d, want 403", code)
|
||||||
|
}
|
||||||
|
if code := post(map[string]string{"Sec-Fetch-Site": "same-site", "Origin": "https://evil.parodia.dev"}); code != http.StatusForbidden {
|
||||||
|
t.Errorf("sibling-subdomain POST = %d, want 403", code)
|
||||||
|
}
|
||||||
|
if code := post(map[string]string{"Sec-Fetch-Site": "same-origin"}); code == http.StatusForbidden {
|
||||||
|
t.Error("same-origin POST was refused")
|
||||||
|
}
|
||||||
|
if code := post(nil); code == http.StatusForbidden {
|
||||||
|
t.Error("headerless server-to-server POST was refused")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,9 +93,6 @@ func TestDealTakesTheStakeAndHidesTheHoleCard(t *testing.T) {
|
|||||||
if code != 200 {
|
if code != 200 {
|
||||||
t.Fatalf("deal = %d, want 200", code)
|
t.Fatalf("deal = %d, want 200", code)
|
||||||
}
|
}
|
||||||
if v.Chips != 900 {
|
|
||||||
t.Fatalf("chips after a 100 bet = %d, want 900", v.Chips)
|
|
||||||
}
|
|
||||||
if v.Hand == nil {
|
if v.Hand == nil {
|
||||||
t.Fatal("deal returned no hand")
|
t.Fatal("deal returned no hand")
|
||||||
}
|
}
|
||||||
@@ -103,10 +100,18 @@ func TestDealTakesTheStakeAndHidesTheHoleCard(t *testing.T) {
|
|||||||
t.Fatalf("player was dealt %d cards, want 2", len(v.Hand.Hands[0].Cards))
|
t.Fatalf("player was dealt %d cards, want 2", len(v.Hand.Hands[0].Cards))
|
||||||
}
|
}
|
||||||
|
|
||||||
// A natural settles on the spot and legitimately shows both dealer cards.
|
// A natural settles on the spot: the stake has already come back (with the
|
||||||
|
// win, or as a push), and both dealer cards are legitimately shown. Checking
|
||||||
|
// the stack for 900 before this made the test fail on every dealt natural.
|
||||||
if v.Hand.Phase == "done" {
|
if v.Hand.Phase == "done" {
|
||||||
|
if v.Chips < 900 {
|
||||||
|
t.Fatalf("chips after a settled natural = %d, want at least the 900 left after the bet", v.Chips)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if v.Chips != 900 {
|
||||||
|
t.Fatalf("chips after a 100 bet = %d, want 900", v.Chips)
|
||||||
|
}
|
||||||
if !v.Hand.Hole || len(v.Hand.Dealer) != 1 {
|
if !v.Hand.Hole || len(v.Hand.Dealer) != 1 {
|
||||||
t.Fatalf("dealer shows %d cards (hole=%v), want 1 with the hole card held back",
|
t.Fatalf("dealer shows %d cards (hole=%v), want 1 with the hole card held back",
|
||||||
len(v.Hand.Dealer), v.Hand.Hole)
|
len(v.Hand.Dealer), v.Hand.Hole)
|
||||||
|
|||||||
@@ -700,6 +700,7 @@ func (s *Server) render(w http.ResponseWriter, page string, data any) {
|
|||||||
|
|
||||||
var funcs = template.FuncMap{
|
var funcs = template.FuncMap{
|
||||||
"thumb": thumbURL,
|
"thumb": thumbURL,
|
||||||
|
"asset": assetURL,
|
||||||
"channel": func(slug string) Channel {
|
"channel": func(slug string) Channel {
|
||||||
for _, ch := range channels {
|
for _, ch := range channels {
|
||||||
if ch.Slug == slug {
|
if ch.Slug == slug {
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ func New(cfg config.WebConfig, sources []config.SourceConfig, postingEnabled boo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticSub))))
|
mux.Handle("GET /static/", http.StripPrefix("/static/", staticHandler(staticSub)))
|
||||||
mux.HandleFunc("GET /img/{name}", s.handleImg)
|
mux.HandleFunc("GET /img/{name}", s.handleImg)
|
||||||
|
|
||||||
mux.HandleFunc("GET /manifest.webmanifest", s.handleManifest)
|
mux.HandleFunc("GET /manifest.webmanifest", s.handleManifest)
|
||||||
@@ -353,9 +353,16 @@ func New(cfg config.WebConfig, sources []config.SourceConfig, postingEnabled boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every state-changing request from a browser must come from this site's
|
||||||
|
// own origin. The session cookie is scoped to the parent domain and is
|
||||||
|
// SameSite=Lax, and every *.parodia.dev host counts as the same site, so
|
||||||
|
// without this a form on any sibling subdomain could bet, order or equip as
|
||||||
|
// whoever is signed in. It keys off Sec-Fetch-Site (falling back to Origin
|
||||||
|
// vs Host) and passes requests carrying neither, so gogobee's bearer-token
|
||||||
|
// calls from Go are untouched. GET/HEAD/OPTIONS are never blocked.
|
||||||
s.srv = &http.Server{
|
s.srv = &http.Server{
|
||||||
Addr: cfg.ListenAddr,
|
Addr: cfg.ListenAddr,
|
||||||
Handler: s.hostRouter(mux),
|
Handler: http.NewCrossOriginProtection().Handler(s.hostRouter(mux)),
|
||||||
ReadHeaderTimeout: 5 * time.Second,
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
}
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
|
|||||||
@@ -17,41 +17,68 @@
|
|||||||
amber-400 on cream at 1.55:1 — half of the four combinations unreadable, and
|
amber-400 on cream at 1.55:1 — half of the four combinations unreadable, and
|
||||||
which half depends on a setting outside the page. Only night has a dark card;
|
which half depends on a setting outside the page. Only night has a dark card;
|
||||||
dusk and dawn are lit despite their names. */
|
dusk and dawn are lit despite their names. */
|
||||||
|
/* Each colour is written once, as bare rgb channels (--ink-rgb), and the plain
|
||||||
|
var (--ink) is built from it. Tailwind needs the channels: tailwind.config.js
|
||||||
|
maps ink/muted/accent/warn/card to rgb(var(--x-rgb) / <alpha-value>), which is
|
||||||
|
what makes text-ink/80 or border-ink/10 actually compile. The old spelling,
|
||||||
|
text-[color:var(--ink)]/80, never did: Tailwind v3 cannot put an opacity on
|
||||||
|
an arbitrary var() colour, so all 538 of those classes silently produced no
|
||||||
|
rule at all.
|
||||||
|
|
||||||
|
--muted is the secondary-text colour: ink mixed into the card, as faint as it
|
||||||
|
can go while still clearing 4.5:1 on both the card and the page background.
|
||||||
|
It exists because ink-at-50% looks right but measures 2.7–2.9:1 on the lit
|
||||||
|
phases. Use text-muted for meta text, not text-ink/50. */
|
||||||
:root,
|
:root,
|
||||||
html[data-phase="day"] {
|
html[data-phase="day"] {
|
||||||
--bg: #fff7e4; /* warm cream */
|
--bg: #fff7e4; /* warm cream */
|
||||||
--bg-grad: #ffeec2; /* soft sun wash */
|
--bg-grad: #ffeec2; /* soft sun wash */
|
||||||
--card: #ffffff;
|
--card-rgb: 255 255 255;
|
||||||
--ink: #3a2e1f;
|
--ink-rgb: 58 46 31;
|
||||||
--accent: #f2a541; /* sunshine yellow */
|
--muted-rgb: 119 111 100; /* 4.95:1 card, 4.64:1 bg */
|
||||||
--warn: #b45309;
|
--accent-rgb: 242 165 65; /* sunshine yellow */
|
||||||
|
--warn-rgb: 180 83 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-phase="dawn"] {
|
html[data-phase="dawn"] {
|
||||||
--bg: #ffe7d6;
|
--bg: #ffe7d6;
|
||||||
--bg-grad: #ffc9c9;
|
--bg-grad: #ffc9c9;
|
||||||
--card: #fff4ea;
|
--card-rgb: 255 244 234;
|
||||||
--ink: #4a2e2a;
|
--ink-rgb: 74 46 42;
|
||||||
--accent: #ff8a65;
|
--muted-rgb: 123 99 94; /* 5.12:1 card, 4.66:1 bg */
|
||||||
--warn: #b45309;
|
--accent-rgb: 255 138 101;
|
||||||
|
--warn-rgb: 180 83 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-phase="dusk"] {
|
html[data-phase="dusk"] {
|
||||||
--bg: #ffd6a8;
|
--bg: #ffd6a8;
|
||||||
--bg-grad: #f7a07e;
|
--bg-grad: #f7a07e;
|
||||||
--card: #fff1de;
|
--card-rgb: 255 241 222;
|
||||||
--ink: #3d2417;
|
--ink-rgb: 61 36 23;
|
||||||
--accent: #e6553a;
|
--muted-rgb: 113 91 77; /* 5.71:1 card, 4.67:1 bg */
|
||||||
--warn: #b45309;
|
--accent-rgb: 230 85 58;
|
||||||
|
--warn-rgb: 180 83 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-phase="night"] {
|
html[data-phase="night"] {
|
||||||
--bg: #1a1f3a;
|
--bg: #1a1f3a;
|
||||||
--bg-grad: #2a3358;
|
--bg-grad: #2a3358;
|
||||||
--card: #2d365a;
|
--card-rgb: 45 54 90;
|
||||||
--ink: #f1ecd8; /* moonlight */
|
--ink-rgb: 241 236 216; /* moonlight */
|
||||||
--accent: #f9d976; /* lantern */
|
--muted-rgb: 163 163 166; /* 4.67:1 card, 6.42:1 bg */
|
||||||
--warn: #fbbf24; /* the only dark card, so the only light amber */
|
--accent-rgb: 249 217 118; /* lantern */
|
||||||
|
--warn-rgb: 251 191 36; /* the only dark card, so the only light amber */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The plain vars, built from the channels. Declared on html alongside every
|
||||||
|
palette block (phase and casino room alike), so a block that only swaps the
|
||||||
|
channels still gets matching plain vars. */
|
||||||
|
html {
|
||||||
|
--card: rgb(var(--card-rgb));
|
||||||
|
--ink: rgb(var(--ink-rgb));
|
||||||
|
--muted: rgb(var(--muted-rgb));
|
||||||
|
--accent: rgb(var(--accent-rgb));
|
||||||
|
--warn: rgb(var(--warn-rgb));
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -81,16 +108,23 @@ html[data-phase="night"] {
|
|||||||
.shadow-pete-lg { box-shadow: 0 6px 0 rgba(60,40,20,0.12), 0 16px 32px rgba(60,40,20,0.12); }
|
.shadow-pete-lg { box-shadow: 0 6px 0 rgba(60,40,20,0.12), 0 16px 32px rgba(60,40,20,0.12); }
|
||||||
|
|
||||||
/* Per-channel theme colors. Kept as plain utilities so Tailwind doesn't
|
/* Per-channel theme colors. Kept as plain utilities so Tailwind doesn't
|
||||||
need to know to safelist them. */
|
need to know to safelist them.
|
||||||
.bg-theme-gaming { background-color: #4caf7d; }
|
|
||||||
.bg-theme-tech { background-color: #5aa9e6; }
|
Every bg-theme-* carries white text (chips, the active nav pill, channel
|
||||||
.bg-theme-politics { background-color: #e07a5f; }
|
heroes), so each one has to clear 4.5:1 against white. The original pastels
|
||||||
|
measured 2.5–3.2:1; these are the same hues taken just dark enough to pass
|
||||||
|
(4.6–4.7:1). eu, lego and adventure already passed and are untouched. The
|
||||||
|
decoration-theme-* underlines below keep the original pastels: they carry
|
||||||
|
no text. */
|
||||||
|
.bg-theme-gaming { background-color: #38815c; } /* was #4caf7d */
|
||||||
|
.bg-theme-tech { background-color: #1d79c0; } /* was #5aa9e6 */
|
||||||
|
.bg-theme-politics { background-color: #cb4a27; } /* was #e07a5f */
|
||||||
.bg-theme-eu { background-color: #003399; }
|
.bg-theme-eu { background-color: #003399; }
|
||||||
.bg-theme-music { background-color: #b079d6; }
|
.bg-theme-music { background-color: #9a54cb; } /* was #b079d6 */
|
||||||
.bg-theme-anime { background-color: #ec5e8a; }
|
.bg-theme-anime { background-color: #e01a58; } /* was #ec5e8a */
|
||||||
.bg-theme-foss { background-color: #d97706; }
|
.bg-theme-foss { background-color: #af6005; } /* was #d97706 */
|
||||||
.bg-theme-kids { background-color: #14b8a6; }
|
.bg-theme-kids { background-color: #0e8376; } /* was #14b8a6 */
|
||||||
.bg-theme-finance { background-color: #10b981; }
|
.bg-theme-finance { background-color: #0c855d; } /* was #10b981 */
|
||||||
.bg-theme-lego { background-color: #d01012; }
|
.bg-theme-lego { background-color: #d01012; }
|
||||||
.bg-theme-adventure { background-color: #6d4bd8; }
|
.bg-theme-adventure { background-color: #6d4bd8; }
|
||||||
|
|
||||||
@@ -2285,9 +2319,10 @@ html[data-phase="night"] {
|
|||||||
|
|
||||||
html[data-room="casinopolis"] {
|
html[data-room="casinopolis"] {
|
||||||
--bg: #16211c; /* the room, lights low */
|
--bg: #16211c; /* the room, lights low */
|
||||||
--card: #23342b; /* a table in it */
|
--card-rgb: 35 52 43; /* a table in it */
|
||||||
--ink: #f6ecd8; /* warm cream, not white */
|
--ink-rgb: 246 236 216; /* warm cream, not white */
|
||||||
--accent: #f2b53d; /* honey */
|
--muted-rgb: 153 155 140; /* 4.65:1 card, 5.85:1 bg */
|
||||||
|
--accent-rgb: 242 181 61; /* honey */
|
||||||
--felt-a: #2f7d5b;
|
--felt-a: #2f7d5b;
|
||||||
--felt-b: #24614a;
|
--felt-b: #24614a;
|
||||||
--felt-c: #1c4d3c;
|
--felt-c: #1c4d3c;
|
||||||
@@ -2296,9 +2331,10 @@ html[data-room="casinopolis"] {
|
|||||||
|
|
||||||
html[data-room="casino-night"] {
|
html[data-room="casino-night"] {
|
||||||
--bg: #140f2e; /* the machine's cabinet */
|
--bg: #140f2e; /* the machine's cabinet */
|
||||||
--card: #241a4d;
|
--card-rgb: 36 26 77;
|
||||||
--ink: #f2ecff;
|
--ink-rgb: 242 236 255;
|
||||||
--accent: #ffcc2f; /* the jackpot bulb */
|
--muted-rgb: 143 135 170; /* 4.67:1 card, 5.46:1 bg */
|
||||||
|
--accent-rgb: 255 204 47; /* the jackpot bulb */
|
||||||
--felt-a: #4a2fa8; /* the table is a pinball table now */
|
--felt-a: #4a2fa8; /* the table is a pinball table now */
|
||||||
--felt-b: #351f80;
|
--felt-b: #351f80;
|
||||||
--felt-c: #241659;
|
--felt-c: #241659;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -156,12 +156,12 @@
|
|||||||
// and pushed the verb itself onto two lines.
|
// and pushed the verb itself onto two lines.
|
||||||
var li = document.createElement('li');
|
var li = document.createElement('li');
|
||||||
var verb = document.createElement('div');
|
var verb = document.createElement('div');
|
||||||
verb.className = 'font-semibold text-[color:var(--ink)]/70';
|
verb.className = 'font-semibold text-muted';
|
||||||
verb.textContent = VERB[o.action] || o.action;
|
verb.textContent = VERB[o.action] || o.action;
|
||||||
var said = document.createElement('div');
|
var said = document.createElement('div');
|
||||||
said.className = 'mt-0.5 leading-snug ' + (o.status === 'pending'
|
said.className = 'mt-0.5 leading-snug ' + (o.status === 'pending'
|
||||||
? 'text-[color:var(--ink)]/45'
|
? 'text-muted'
|
||||||
: (o.status === 'applied' ? 'text-theme-adventure font-semibold' : 'text-[color:var(--warn)]'));
|
: (o.status === 'applied' ? 'text-theme-adventure font-semibold' : 'text-warn'));
|
||||||
said.textContent = o.detail || STATUS[o.status] || o.status;
|
said.textContent = o.detail || STATUS[o.status] || o.status;
|
||||||
li.appendChild(verb); li.appendChild(said);
|
li.appendChild(verb); li.appendChild(said);
|
||||||
list.appendChild(li);
|
list.appendChild(li);
|
||||||
@@ -247,14 +247,14 @@
|
|||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
|
|
||||||
var boxEl = document.createElement('div');
|
var boxEl = document.createElement('div');
|
||||||
boxEl.className = 'adv-action-confirm mt-3 rounded-xl bg-[color:var(--ink)]/5 p-3 text-sm';
|
boxEl.className = 'adv-action-confirm mt-3 rounded-xl bg-ink/5 p-3 text-sm';
|
||||||
var p = document.createElement('p');
|
var p = document.createElement('p');
|
||||||
p.className = 'text-[color:var(--ink)]/70';
|
p.className = 'text-muted';
|
||||||
p.textContent = btn.getAttribute('data-confirm') || 'Are you sure?';
|
p.textContent = btn.getAttribute('data-confirm') || 'Are you sure?';
|
||||||
var cost = parseFloat(btn.getAttribute('data-cost') || '0');
|
var cost = parseFloat(btn.getAttribute('data-cost') || '0');
|
||||||
if (cost > 0) {
|
if (cost > 0) {
|
||||||
var money = document.createElement('p');
|
var money = document.createElement('p');
|
||||||
money.className = 'mt-1.5 font-semibold text-[color:var(--ink)]/80';
|
money.className = 'mt-1.5 font-semibold text-ink/80';
|
||||||
if (panelBalance - cost < 0) {
|
if (panelBalance - cost < 0) {
|
||||||
// No arrow when it does not cover. The game can carry a small debt, so
|
// No arrow when it does not cover. The game can carry a small debt, so
|
||||||
// the resulting figure is not always nonsense — but printing "€-6,300"
|
// the resulting figure is not always nonsense — but printing "€-6,300"
|
||||||
@@ -262,7 +262,7 @@
|
|||||||
// lands on the wrong side of the sign.
|
// lands on the wrong side of the sign.
|
||||||
money.textContent = '€' + euroFmt(cost) + " — you have €" + euroFmt(panelBalance) +
|
money.textContent = '€' + euroFmt(cost) + " — you have €" + euroFmt(panelBalance) +
|
||||||
". That won't cover it.";
|
". That won't cover it.";
|
||||||
money.className += ' text-[color:var(--warn)]';
|
money.className += ' text-warn';
|
||||||
} else {
|
} else {
|
||||||
money.textContent = '€' + euroFmt(cost) + ' — balance €' + euroFmt(panelBalance) +
|
money.textContent = '€' + euroFmt(cost) + ' — balance €' + euroFmt(panelBalance) +
|
||||||
' → €' + euroFmt(panelBalance - cost) + '.';
|
' → €' + euroFmt(panelBalance - cost) + '.';
|
||||||
@@ -293,7 +293,7 @@
|
|||||||
yes.addEventListener('click', function () { boxEl.remove(); placeOrder(btn); });
|
yes.addEventListener('click', function () { boxEl.remove(); placeOrder(btn); });
|
||||||
var no = document.createElement('button');
|
var no = document.createElement('button');
|
||||||
no.type = 'button';
|
no.type = 'button';
|
||||||
no.className = 'rounded-full border border-[color:var(--ink)]/20 text-[color:var(--ink)]/60 px-3 py-1';
|
no.className = 'rounded-full border border-ink/20 text-muted px-3 py-1';
|
||||||
no.textContent = 'Not yet';
|
no.textContent = 'Not yet';
|
||||||
no.addEventListener('click', function () { boxEl.remove(); });
|
no.addEventListener('click', function () { boxEl.remove(); });
|
||||||
row.appendChild(yes); row.appendChild(no);
|
row.appendChild(yes); row.appendChild(no);
|
||||||
|
|||||||
@@ -844,12 +844,12 @@
|
|||||||
var stakes = tierBtns.filter(function (b) { return b.dataset.tier === t.tier; })[0];
|
var stakes = tierBtns.filter(function (b) { return b.dataset.tier === t.tier; })[0];
|
||||||
var btn = document.createElement("button");
|
var btn = document.createElement("button");
|
||||||
btn.type = "button";
|
btn.type = "button";
|
||||||
btn.className = "flex items-center justify-between gap-3 rounded-2xl border-2 border-[color:var(--ink)]/10 p-3 text-left hover:bg-[color:var(--ink)]/5 transition";
|
btn.className = "flex items-center justify-between gap-3 rounded-2xl border-2 border-ink/10 p-3 text-left hover:bg-ink/5 transition";
|
||||||
var label = document.createElement("span");
|
var label = document.createElement("span");
|
||||||
label.className = "font-display font-bold";
|
label.className = "font-display font-bold";
|
||||||
label.textContent = stakes ? stakes.querySelector(".font-display").textContent : t.tier;
|
label.textContent = stakes ? stakes.querySelector(".font-display").textContent : t.tier;
|
||||||
var seats = document.createElement("span");
|
var seats = document.createElement("span");
|
||||||
seats.className = "text-xs font-semibold text-[color:var(--ink)]/50 tabular-nums";
|
seats.className = "text-xs font-semibold text-muted tabular-nums";
|
||||||
seats.textContent = t.humans + "/" + t.seats + " seated";
|
seats.textContent = t.humans + "/" + t.seats + " seated";
|
||||||
btn.appendChild(label);
|
btn.appendChild(label);
|
||||||
btn.appendChild(seats);
|
btn.appendChild(seats);
|
||||||
|
|||||||
@@ -58,29 +58,29 @@
|
|||||||
}
|
}
|
||||||
const html = items.map((r, i) => {
|
const html = items.map((r, i) => {
|
||||||
const thumb = r.thumb_url
|
const thumb = r.thumb_url
|
||||||
? `<div class="hidden sm:block w-20 h-20 shrink-0 overflow-hidden rounded-2xl bg-[color:var(--ink)]/5">
|
? `<div class="hidden sm:block w-20 h-20 shrink-0 overflow-hidden rounded-2xl bg-ink/5">
|
||||||
<img src="${escapeHTML(r.thumb_url)}" alt="${escapeHTML(r.headline || "")}" loading="lazy" decoding="async"
|
<img src="${escapeHTML(r.thumb_url)}" alt="${escapeHTML(r.headline || "")}" loading="lazy" decoding="async"
|
||||||
class="h-full w-full object-cover">
|
class="h-full w-full object-cover">
|
||||||
</div>`
|
</div>`
|
||||||
: `<div class="hidden sm:flex w-20 h-20 shrink-0 items-center justify-center rounded-2xl bg-[color:var(--ink)]/5 text-2xl">${escapeHTML(r.channel_emoji || "·")}</div>`;
|
: `<div class="hidden sm:flex w-20 h-20 shrink-0 items-center justify-center rounded-2xl bg-ink/5 text-2xl">${escapeHTML(r.channel_emoji || "·")}</div>`;
|
||||||
const postedRing = r.posted ? ` border-theme-${escapeHTML(r.channel_theme)}` : "";
|
const postedRing = r.posted ? ` border-theme-${escapeHTML(r.channel_theme)}` : "";
|
||||||
const href = safeURL(r.article_url);
|
const href = safeURL(r.article_url);
|
||||||
if (!href) return "";
|
if (!href) return "";
|
||||||
return `
|
return `
|
||||||
<a href="${escapeHTML(href)}" target="_blank" rel="noopener noreferrer"
|
<a href="${escapeHTML(href)}" target="_blank" rel="noopener noreferrer"
|
||||||
data-idx="${i}"
|
data-idx="${i}"
|
||||||
class="search-result group flex items-start gap-4 rounded-2xl p-3 transition border-2 border-transparent hover:bg-[color:var(--ink)]/5${postedRing}">
|
class="search-result group flex items-start gap-4 rounded-2xl p-3 transition border-2 border-transparent hover:bg-ink/5${postedRing}">
|
||||||
${thumb}
|
${thumb}
|
||||||
<div class="min-w-0 flex-1 space-y-1">
|
<div class="min-w-0 flex-1 space-y-1">
|
||||||
<div class="flex items-center gap-2 text-xs flex-wrap">
|
<div class="flex items-center gap-2 text-xs flex-wrap">
|
||||||
<span class="inline-flex items-center rounded-full bg-theme-${escapeHTML(r.channel_theme)} text-white px-2.5 py-0.5 font-semibold uppercase tracking-wider">
|
<span class="inline-flex items-center rounded-full bg-theme-${escapeHTML(r.channel_theme)} text-white px-2.5 py-0.5 font-semibold uppercase tracking-wider">
|
||||||
<span aria-hidden="true" class="mr-1">${escapeHTML(r.channel_emoji)}</span>${escapeHTML(r.channel_title)}
|
<span aria-hidden="true" class="mr-1">${escapeHTML(r.channel_emoji)}</span>${escapeHTML(r.channel_title)}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[color:var(--ink)]/60 font-semibold">${escapeHTML(r.source)}</span>
|
<span class="text-muted font-semibold">${escapeHTML(r.source)}</span>
|
||||||
<span class="text-[color:var(--ink)]/50">${escapeHTML(r.time_ago)}</span>
|
<span class="text-muted">${escapeHTML(r.time_ago)}</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="font-display text-base sm:text-lg font-semibold leading-snug">${escapeHTML(r.headline)}</h3>
|
<h3 class="font-display text-base sm:text-lg font-semibold leading-snug">${escapeHTML(r.headline)}</h3>
|
||||||
${r.lede ? `<p class="text-sm text-[color:var(--ink)]/70 line-clamp-2">${escapeHTML(r.lede)}</p>` : ""}
|
${r.lede ? `<p class="text-sm text-muted line-clamp-2">${escapeHTML(r.lede)}</p>` : ""}
|
||||||
</div>
|
</div>
|
||||||
</a>`;
|
</a>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
@@ -92,10 +92,10 @@
|
|||||||
const nodes = list.querySelectorAll(".search-result");
|
const nodes = list.querySelectorAll(".search-result");
|
||||||
nodes.forEach((n, i) => {
|
nodes.forEach((n, i) => {
|
||||||
if (i === activeIndex) {
|
if (i === activeIndex) {
|
||||||
n.classList.add("bg-[color:var(--ink)]/5", "border-[color:var(--ink)]/15");
|
n.classList.add("bg-ink/5", "border-ink/15");
|
||||||
n.scrollIntoView({ block: "nearest" });
|
n.scrollIntoView({ block: "nearest" });
|
||||||
} else {
|
} else {
|
||||||
n.classList.remove("bg-[color:var(--ink)]/5", "border-[color:var(--ink)]/15");
|
n.classList.remove("bg-ink/5", "border-ink/15");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,28 +67,28 @@
|
|||||||
var sources = knownSources();
|
var sources = knownSources();
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
if (sources.length === 0) {
|
if (sources.length === 0) {
|
||||||
list.innerHTML = '<p class="px-2 py-4 text-sm text-center text-[color:var(--ink)]/50">No feeds configured.</p>';
|
list.innerHTML = '<p class="px-2 py-4 text-sm text-center text-muted">No feeds configured.</p>';
|
||||||
} else {
|
} else {
|
||||||
var lastChannel = null;
|
var lastChannel = null;
|
||||||
sources.forEach(function (src, i) {
|
sources.forEach(function (src, i) {
|
||||||
if (src.channel !== lastChannel) {
|
if (src.channel !== lastChannel) {
|
||||||
lastChannel = src.channel;
|
lastChannel = src.channel;
|
||||||
var hdr = document.createElement("div");
|
var hdr = document.createElement("div");
|
||||||
hdr.className = "px-3 pt-3 pb-1 text-[11px] uppercase tracking-wider text-[color:var(--ink)]/40 font-bold";
|
hdr.className = "px-3 pt-3 pb-1 text-[11px] uppercase tracking-wider text-muted font-bold";
|
||||||
hdr.textContent = src.channel || "other";
|
hdr.textContent = src.channel || "other";
|
||||||
list.appendChild(hdr);
|
list.appendChild(hdr);
|
||||||
}
|
}
|
||||||
var id = "pete-feed-" + i;
|
var id = "pete-feed-" + i;
|
||||||
var row = document.createElement("label");
|
var row = document.createElement("label");
|
||||||
row.setAttribute("for", id);
|
row.setAttribute("for", id);
|
||||||
row.className = "flex items-center justify-between gap-3 rounded-2xl px-3 py-2 hover:bg-[color:var(--ink)]/5 cursor-pointer";
|
row.className = "flex items-center justify-between gap-3 rounded-2xl px-3 py-2 hover:bg-ink/5 cursor-pointer";
|
||||||
var nameEl = document.createElement("span");
|
var nameEl = document.createElement("span");
|
||||||
nameEl.className = "text-sm font-semibold truncate";
|
nameEl.className = "text-sm font-semibold truncate";
|
||||||
nameEl.textContent = src.name;
|
nameEl.textContent = src.name;
|
||||||
var box = document.createElement("input");
|
var box = document.createElement("input");
|
||||||
box.type = "checkbox";
|
box.type = "checkbox";
|
||||||
box.id = id;
|
box.id = id;
|
||||||
box.className = "h-4 w-4 shrink-0 accent-[color:var(--accent)]";
|
box.className = "h-4 w-4 shrink-0 accent-accent";
|
||||||
box.checked = !disabled[src.name];
|
box.checked = !disabled[src.name];
|
||||||
box.addEventListener("change", function () {
|
box.addEventListener("change", function () {
|
||||||
if (box.checked) delete disabled[src.name];
|
if (box.checked) delete disabled[src.name];
|
||||||
|
|||||||
@@ -1018,12 +1018,12 @@
|
|||||||
var stakes = root.querySelector('[data-tier="' + t.tier + '"]');
|
var stakes = root.querySelector('[data-tier="' + t.tier + '"]');
|
||||||
var btn = document.createElement("button");
|
var btn = document.createElement("button");
|
||||||
btn.type = "button";
|
btn.type = "button";
|
||||||
btn.className = "flex items-center justify-between gap-3 rounded-2xl border-2 border-[color:var(--ink)]/10 p-3 text-left hover:bg-[color:var(--ink)]/5 transition";
|
btn.className = "flex items-center justify-between gap-3 rounded-2xl border-2 border-ink/10 p-3 text-left hover:bg-ink/5 transition";
|
||||||
var lbl = document.createElement("span");
|
var lbl = document.createElement("span");
|
||||||
lbl.className = "font-display font-bold";
|
lbl.className = "font-display font-bold";
|
||||||
lbl.textContent = stakes ? stakes.querySelector(".font-display").textContent : t.tier;
|
lbl.textContent = stakes ? stakes.querySelector(".font-display").textContent : t.tier;
|
||||||
var seats = document.createElement("span");
|
var seats = document.createElement("span");
|
||||||
seats.className = "text-xs font-semibold text-[color:var(--ink)]/50 tabular-nums";
|
seats.className = "text-xs font-semibold text-muted tabular-nums";
|
||||||
seats.textContent = t.humans + "/" + t.seats + " seated";
|
seats.textContent = t.humans + "/" + t.seats + " seated";
|
||||||
btn.appendChild(lbl);
|
btn.appendChild(lbl);
|
||||||
btn.appendChild(seats);
|
btn.appendChild(seats);
|
||||||
|
|||||||
@@ -207,10 +207,10 @@
|
|||||||
if (!cardMount) return;
|
if (!cardMount) return;
|
||||||
var air = describeAqi(entry.aqi);
|
var air = describeAqi(entry.aqi);
|
||||||
var aqiRow = air
|
var aqiRow = air
|
||||||
? '<div class="flex items-center gap-2 border-t border-[color:var(--ink)]/10 px-5 py-3 text-sm">' +
|
? '<div class="flex items-center gap-2 border-t border-ink/10 px-5 py-3 text-sm">' +
|
||||||
'<span class="inline-block w-2.5 h-2.5 rounded-full shrink-0" style="background:' + air.color + '"></span>' +
|
'<span class="inline-block w-2.5 h-2.5 rounded-full shrink-0" style="background:' + air.color + '"></span>' +
|
||||||
'<span class="font-bold tabular-nums">AQI ' + entry.aqi + '</span>' +
|
'<span class="font-bold tabular-nums">AQI ' + entry.aqi + '</span>' +
|
||||||
'<span class="text-[color:var(--ink)]/60">' + air.label + '</span>' +
|
'<span class="text-muted">' + air.label + '</span>' +
|
||||||
'</div>'
|
'</div>'
|
||||||
: '';
|
: '';
|
||||||
var daily = entry.daily;
|
var daily = entry.daily;
|
||||||
@@ -219,28 +219,28 @@
|
|||||||
var dd = describe(daily.weather_code[i], 1);
|
var dd = describe(daily.weather_code[i], 1);
|
||||||
days +=
|
days +=
|
||||||
'<div class="flex flex-col items-center gap-1 rounded-2xl px-2 py-2 min-w-0">' +
|
'<div class="flex flex-col items-center gap-1 rounded-2xl px-2 py-2 min-w-0">' +
|
||||||
'<span class="text-xs font-semibold text-[color:var(--ink)]/60">' + (i === 0 ? "Today" : weekday(daily.time[i])) + '</span>' +
|
'<span class="text-xs font-semibold text-muted">' + (i === 0 ? "Today" : weekday(daily.time[i])) + '</span>' +
|
||||||
'<span class="text-2xl leading-none" aria-hidden="true">' + dd.emoji + '</span>' +
|
'<span class="text-2xl leading-none" aria-hidden="true">' + dd.emoji + '</span>' +
|
||||||
'<span class="text-sm font-bold tabular-nums">' + Math.round(daily.temperature_2m_max[i]) + '°</span>' +
|
'<span class="text-sm font-bold tabular-nums">' + Math.round(daily.temperature_2m_max[i]) + '°</span>' +
|
||||||
'<span class="text-xs tabular-nums text-[color:var(--ink)]/50">' + Math.round(daily.temperature_2m_min[i]) + '°</span>' +
|
'<span class="text-xs tabular-nums text-muted">' + Math.round(daily.temperature_2m_min[i]) + '°</span>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
cardMount.innerHTML =
|
cardMount.innerHTML =
|
||||||
'<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 shadow-pete overflow-hidden">' +
|
'<div class="rounded-3xl bg-card border-2 border-ink/10 shadow-pete overflow-hidden">' +
|
||||||
'<div class="flex flex-wrap items-center justify-between gap-4 p-5">' +
|
'<div class="flex flex-wrap items-center justify-between gap-4 p-5">' +
|
||||||
'<div class="flex items-center gap-4 min-w-0">' +
|
'<div class="flex items-center gap-4 min-w-0">' +
|
||||||
'<span class="text-5xl leading-none" aria-hidden="true">' + desc.emoji + '</span>' +
|
'<span class="text-5xl leading-none" aria-hidden="true">' + desc.emoji + '</span>' +
|
||||||
'<div class="min-w-0">' +
|
'<div class="min-w-0">' +
|
||||||
'<div class="flex items-baseline gap-2">' +
|
'<div class="flex items-baseline gap-2">' +
|
||||||
'<span class="font-display text-4xl font-bold tabular-nums">' + temp(cur.temperature_2m, sym) + '</span>' +
|
'<span class="font-display text-4xl font-bold tabular-nums">' + temp(cur.temperature_2m, sym) + '</span>' +
|
||||||
'<span class="text-sm font-semibold text-[color:var(--ink)]/70 truncate">' + desc.label + '</span>' +
|
'<span class="text-sm font-semibold text-muted truncate">' + desc.label + '</span>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="text-sm text-[color:var(--ink)]/60 truncate">📍 ' + escapeHtml(loc.name) + '</div>' +
|
'<div class="text-sm text-muted truncate">📍 ' + escapeHtml(loc.name) + '</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<button type="button" data-weather-loc class="shrink-0 rounded-full px-3 py-1.5 text-xs font-semibold text-[color:var(--ink)]/60 hover:bg-[color:var(--ink)]/5 border-2 border-[color:var(--ink)]/10 transition">Change location</button>' +
|
'<button type="button" data-weather-loc class="shrink-0 rounded-full px-3 py-1.5 text-xs font-semibold text-muted hover:bg-ink/5 border-2 border-ink/10 transition">Change location</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="grid grid-cols-5 gap-1 border-t border-[color:var(--ink)]/10 bg-[color:var(--ink)]/[0.03] px-2 py-3">' + days + '</div>' +
|
'<div class="grid grid-cols-5 gap-1 border-t border-ink/10 bg-ink/[0.03] px-2 py-3">' + days + '</div>' +
|
||||||
aqiRow +
|
aqiRow +
|
||||||
'</div>';
|
'</div>';
|
||||||
// The injected "Change location" button needs the same handler.
|
// The injected "Change location" button needs the same handler.
|
||||||
@@ -259,7 +259,7 @@
|
|||||||
setAqiChip(null);
|
setAqiChip(null);
|
||||||
if (cardMount && getLoc()) {
|
if (cardMount && getLoc()) {
|
||||||
cardMount.innerHTML =
|
cardMount.innerHTML =
|
||||||
'<div class="rounded-3xl bg-[color:var(--card)] border-2 border-dashed border-[color:var(--ink)]/15 p-5 text-center text-sm text-[color:var(--ink)]/60">' +
|
'<div class="rounded-3xl bg-card border-2 border-dashed border-ink/15 p-5 text-center text-sm text-muted">' +
|
||||||
escapeHtml(msg) + ' · <button type="button" data-weather-loc class="font-semibold underline">try again</button>' +
|
escapeHtml(msg) + ' · <button type="button" data-weather-loc class="font-semibold underline">try again</button>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
var b = cardMount.querySelector("[data-weather-loc]");
|
var b = cardMount.querySelector("[data-weather-loc]");
|
||||||
@@ -278,9 +278,9 @@
|
|||||||
if (!popover) return;
|
if (!popover) return;
|
||||||
popover.querySelectorAll("[data-loc-unit]").forEach(function (b) {
|
popover.querySelectorAll("[data-loc-unit]").forEach(function (b) {
|
||||||
var on = b.getAttribute("data-loc-unit") === selectedUnit;
|
var on = b.getAttribute("data-loc-unit") === selectedUnit;
|
||||||
b.classList.toggle("bg-[color:var(--accent)]", on);
|
b.classList.toggle("bg-accent", on);
|
||||||
b.classList.toggle("text-white", on);
|
b.classList.toggle("text-white", on);
|
||||||
b.classList.toggle("text-[color:var(--ink)]/60", !on);
|
b.classList.toggle("text-muted", !on);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,32 +293,32 @@
|
|||||||
function buildPopover() {
|
function buildPopover() {
|
||||||
var dlg = document.createElement("div");
|
var dlg = document.createElement("div");
|
||||||
dlg.id = "pete-weather-loc";
|
dlg.id = "pete-weather-loc";
|
||||||
dlg.className = "hidden fixed inset-0 z-50 bg-[color:var(--ink)]/40 backdrop-blur-sm flex items-start justify-center pt-[12vh] px-4";
|
dlg.className = "hidden fixed inset-0 z-50 bg-ink/40 backdrop-blur-sm flex items-start justify-center pt-[12vh] px-4";
|
||||||
dlg.setAttribute("role", "dialog");
|
dlg.setAttribute("role", "dialog");
|
||||||
dlg.setAttribute("aria-modal", "true");
|
dlg.setAttribute("aria-modal", "true");
|
||||||
dlg.innerHTML =
|
dlg.innerHTML =
|
||||||
'<div class="w-full max-w-sm rounded-3xl bg-[color:var(--card)] shadow-pete-lg border-2 border-[color:var(--ink)]/10 overflow-hidden">' +
|
'<div class="w-full max-w-sm rounded-3xl bg-card shadow-pete-lg border-2 border-ink/10 overflow-hidden">' +
|
||||||
'<div class="flex items-center justify-between gap-3 px-5 py-4 border-b border-[color:var(--ink)]/10">' +
|
'<div class="flex items-center justify-between gap-3 px-5 py-4 border-b border-ink/10">' +
|
||||||
'<h2 class="font-display text-lg font-bold">Weather location</h2>' +
|
'<h2 class="font-display text-lg font-bold">Weather location</h2>' +
|
||||||
'<button type="button" data-loc-close class="rounded-full px-2 py-1 text-sm text-[color:var(--ink)]/60 hover:bg-[color:var(--ink)]/5">esc</button>' +
|
'<button type="button" data-loc-close class="rounded-full px-2 py-1 text-sm text-muted hover:bg-ink/5">esc</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="p-5 space-y-3">' +
|
'<div class="p-5 space-y-3">' +
|
||||||
'<p class="text-xs text-[color:var(--ink)]/60">Enter your postal code to see the local forecast. The background follows the weather. Saved in this browser only.</p>' +
|
'<p class="text-xs text-muted">Enter your postal code to see the local forecast. The background follows the weather. Saved in this browser only.</p>' +
|
||||||
'<div class="flex gap-2">' +
|
'<div class="flex gap-2">' +
|
||||||
'<select data-loc-country class="rounded-2xl border-2 border-[color:var(--ink)]/10 bg-[color:var(--bg)] px-3 py-2 text-sm font-semibold outline-none focus:border-[color:var(--accent)]"></select>' +
|
'<select data-loc-country class="rounded-2xl border-2 border-ink/10 bg-[color:var(--bg)] px-3 py-2 text-sm font-semibold outline-none focus:border-accent"></select>' +
|
||||||
'<input data-loc-postal type="text" inputmode="text" autocomplete="postal-code" placeholder="Postal code" class="flex-1 min-w-0 rounded-2xl border-2 border-[color:var(--ink)]/10 bg-[color:var(--bg)] px-3 py-2 text-sm outline-none focus:border-[color:var(--accent)]">' +
|
'<input data-loc-postal type="text" inputmode="text" autocomplete="postal-code" placeholder="Postal code" class="flex-1 min-w-0 rounded-2xl border-2 border-ink/10 bg-[color:var(--bg)] px-3 py-2 text-sm outline-none focus:border-accent">' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="flex items-center justify-between gap-2">' +
|
'<div class="flex items-center justify-between gap-2">' +
|
||||||
'<span class="text-xs font-semibold text-[color:var(--ink)]/60">Units</span>' +
|
'<span class="text-xs font-semibold text-muted">Units</span>' +
|
||||||
'<div class="inline-flex rounded-full border-2 border-[color:var(--ink)]/10 p-0.5 text-xs font-bold">' +
|
'<div class="inline-flex rounded-full border-2 border-ink/10 p-0.5 text-xs font-bold">' +
|
||||||
'<button type="button" data-loc-unit="celsius" class="rounded-full px-3 py-1 transition">°C</button>' +
|
'<button type="button" data-loc-unit="celsius" class="rounded-full px-3 py-1 transition">°C</button>' +
|
||||||
'<button type="button" data-loc-unit="fahrenheit" class="rounded-full px-3 py-1 transition">°F</button>' +
|
'<button type="button" data-loc-unit="fahrenheit" class="rounded-full px-3 py-1 transition">°F</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<p data-loc-error class="hidden text-xs font-semibold text-red-500"></p>' +
|
'<p data-loc-error class="hidden text-xs font-semibold text-red-500"></p>' +
|
||||||
'<div class="flex items-center justify-between gap-2 pt-1">' +
|
'<div class="flex items-center justify-between gap-2 pt-1">' +
|
||||||
'<button type="button" data-loc-clear class="rounded-full px-3 py-2 text-xs font-semibold text-[color:var(--ink)]/50 hover:text-[color:var(--ink)]">Clear</button>' +
|
'<button type="button" data-loc-clear class="rounded-full px-3 py-2 text-xs font-semibold text-muted hover:text-ink">Clear</button>' +
|
||||||
'<button type="button" data-loc-save class="rounded-full bg-[color:var(--accent)] px-5 py-2 text-sm font-bold text-white shadow-pete hover:brightness-105 transition disabled:opacity-50">Save</button>' +
|
'<button type="button" data-loc-save class="rounded-full bg-accent px-5 py-2 text-sm font-bold text-white shadow-pete hover:brightness-105 transition disabled:opacity-50">Save</button>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
|
|||||||
+34
-16
@@ -4,12 +4,13 @@
|
|||||||
//
|
//
|
||||||
// Bump CACHE_VERSION whenever the precached shell assets change; activate()
|
// Bump CACHE_VERSION whenever the precached shell assets change; activate()
|
||||||
// drops every cache that doesn't match the current version.
|
// drops every cache that doesn't match the current version.
|
||||||
var CACHE_VERSION = "v5";
|
var CACHE_VERSION = "v6";
|
||||||
var SHELL_CACHE = "pete-shell-" + CACHE_VERSION;
|
var SHELL_CACHE = "pete-shell-" + CACHE_VERSION;
|
||||||
var RUNTIME_CACHE = "pete-runtime-" + CACHE_VERSION;
|
var RUNTIME_CACHE = "pete-runtime-" + CACHE_VERSION;
|
||||||
|
|
||||||
// App shell: the static assets every page needs. Versioned by URL content only
|
// App shell: the static assets every page needs, precached by bare URL so an
|
||||||
// loosely, so we lean on network-first for HTML and cache-first for these.
|
// offline launch has something to fall back on. Pages ask for them with a
|
||||||
|
// ?v=<hash> suffix; see the fetch handler for how the two meet.
|
||||||
var SHELL_ASSETS = [
|
var SHELL_ASSETS = [
|
||||||
"/static/css/output.css",
|
"/static/css/output.css",
|
||||||
"/static/js/prefs.js",
|
"/static/js/prefs.js",
|
||||||
@@ -117,25 +118,42 @@ self.addEventListener("fetch", function (event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static assets: network-first. Our asset URLs are NOT content-hashed
|
// Static assets. Pages link them as /static/...?v=<content hash> (the
|
||||||
// (/static/js/weather-gl.js stays the same URL across deploys), so a
|
// {{asset}} template func), so a versioned URL names exactly one set of bytes
|
||||||
// cache-first strategy would pin whatever bytes were first cached and never
|
// and is safe to serve cache-first: a redeploy that changes the file changes
|
||||||
// notice a redeployed file changed — leaving stale JS/CSS in place until the
|
// the URL. When one is cached, older versions of the same file are dropped so
|
||||||
// CACHE_VERSION bump below. Go's FileServer sets ETag/Last-Modified, so an
|
// the shell cache doesn't grow by a copy per deploy.
|
||||||
// online refetch is a cheap 304 when nothing changed. We still fill the cache
|
//
|
||||||
// on every success and fall back to it when the network is unreachable, so
|
// Anything requested by a bare URL (the precache list above, audio samples)
|
||||||
// offline reading keeps the shell it needs.
|
// stays network-first; the server sends those no-cache with a content-hash
|
||||||
|
// ETag, so the refetch is a 304 when nothing changed. Offline, either kind
|
||||||
|
// falls back to whatever copy of that path is cached, whatever its version.
|
||||||
if (url.pathname.indexOf("/static/") === 0) {
|
if (url.pathname.indexOf("/static/") === 0) {
|
||||||
event.respondWith(
|
var versioned = url.searchParams.has("v");
|
||||||
fetch(req).then(function (res) {
|
var fromNetwork = function () {
|
||||||
|
return fetch(req).then(function (res) {
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
var copy = res.clone();
|
var copy = res.clone();
|
||||||
caches.open(SHELL_CACHE).then(function (cache) { cache.put(req, copy); });
|
caches.open(SHELL_CACHE).then(function (cache) {
|
||||||
|
if (!versioned) return cache.put(req, copy);
|
||||||
|
return cache.keys().then(function (keys) {
|
||||||
|
keys.forEach(function (k) {
|
||||||
|
var ku = new URL(k.url);
|
||||||
|
if (ku.pathname === url.pathname && ku.search !== url.search) cache.delete(k);
|
||||||
|
});
|
||||||
|
return cache.put(req, copy);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
return caches.match(req);
|
return caches.match(req, { ignoreSearch: true });
|
||||||
})
|
});
|
||||||
|
};
|
||||||
|
event.respondWith(
|
||||||
|
versioned
|
||||||
|
? caches.match(req).then(function (hit) { return hit || fromNetwork(); })
|
||||||
|
: fromNetwork()
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
a class: the palette is a Go table of hex values, and a generated
|
a class: the palette is a Go table of hex values, and a generated
|
||||||
Tailwind class would be purged out of the stylesheet and fail silently. */}}
|
Tailwind class would be purged out of the stylesheet and fail silently. */}}
|
||||||
{{if .Story.Accent}}style="border-color:{{.Story.Accent}}{{if .Story.Ceremony}};box-shadow:0 0 0 4px {{.Story.Accent}}33, 0 10px 30px -12px {{.Story.Accent}}{{end}}"{{end}}
|
{{if .Story.Accent}}style="border-color:{{.Story.Accent}}{{if .Story.Ceremony}};box-shadow:0 0 0 4px {{.Story.Accent}}33, 0 10px 30px -12px {{.Story.Accent}}{{end}}"{{end}}
|
||||||
class="group relative block rounded-3xl bg-[color:var(--card)] border-2 {{if .Story.Posted}}border-theme-{{.Theme}} glow-theme-{{.Theme}}{{else}}border-[color:var(--ink)]/10{{end}} shadow-pete overflow-hidden hover:-translate-y-1 hover:shadow-pete-lg transition">
|
class="group relative block rounded-3xl bg-card border-2 {{if .Story.Posted}}border-theme-{{.Theme}} glow-theme-{{.Theme}}{{else}}border-ink/10{{end}} shadow-pete overflow-hidden hover:-translate-y-1 hover:shadow-pete-lg transition">
|
||||||
<span role="button" tabindex="0" data-bookmark-btn data-story-id="{{.Story.ID}}"
|
<span role="button" tabindex="0" data-bookmark-btn data-story-id="{{.Story.ID}}"
|
||||||
aria-label="Bookmark this story" aria-pressed="false"
|
aria-label="Bookmark this story" aria-pressed="false"
|
||||||
style="display:none;position:absolute;top:.6rem;left:.6rem;z-index:6;height:1.9rem;width:1.9rem;align-items:center;justify-content:center;border-radius:9999px;background:rgba(20,14,6,.62);color:#fff;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)">
|
style="display:none;position:absolute;top:.6rem;left:.6rem;z-index:6;height:1.9rem;width:1.9rem;align-items:center;justify-content:center;border-radius:9999px;background:rgba(20,14,6,.62);color:#fff;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)">
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
{{if .Story.ImageURL}}
|
{{if .Story.ImageURL}}
|
||||||
<div class="aspect-[16/10] w-full overflow-hidden bg-[color:var(--ink)]/5">
|
<div class="aspect-[16/10] w-full overflow-hidden bg-ink/5">
|
||||||
<img src="{{thumb .Story.ImageURL}}" alt="{{.Story.Headline}}" loading="lazy" decoding="async"
|
<img src="{{thumb .Story.ImageURL}}" alt="{{.Story.Headline}}" loading="lazy" decoding="async"
|
||||||
class="h-full w-full object-cover group-hover:scale-105 transition duration-500">
|
class="h-full w-full object-cover group-hover:scale-105 transition duration-500">
|
||||||
</div>
|
</div>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<span class="inline-flex items-center rounded-full bg-theme-{{$ch.Theme}} text-white px-2.5 py-0.5 font-semibold uppercase tracking-wider">
|
<span class="inline-flex items-center rounded-full bg-theme-{{$ch.Theme}} text-white px-2.5 py-0.5 font-semibold uppercase tracking-wider">
|
||||||
<span aria-hidden="true" class="mr-1">{{$ch.Emoji}}</span>{{$ch.Title}}
|
<span aria-hidden="true" class="mr-1">{{$ch.Emoji}}</span>{{$ch.Title}}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[color:var(--ink)]/60 font-semibold">{{.Story.Source}}</span>
|
<span class="text-muted font-semibold">{{.Story.Source}}</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="inline-flex items-center rounded-full bg-theme-{{.Theme}} text-white px-2.5 py-0.5 font-semibold uppercase tracking-wider">
|
<span class="inline-flex items-center rounded-full bg-theme-{{.Theme}} text-white px-2.5 py-0.5 font-semibold uppercase tracking-wider">
|
||||||
{{.Story.Source}}
|
{{.Story.Source}}
|
||||||
@@ -47,9 +47,9 @@
|
|||||||
<span aria-hidden="true" class="mr-1">★</span>Realm first
|
<span aria-hidden="true" class="mr-1">★</span>Realm first
|
||||||
</span>
|
</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
<span class="text-[color:var(--ink)]/50">{{timeAgo .Story.SeenAt}}</span>
|
<span class="text-muted">{{timeAgo .Story.SeenAt}}</span>
|
||||||
{{if .Story.ReadMins}}<span class="text-[color:var(--ink)]/45">· {{.Story.ReadMins}} min read</span>{{end}}
|
{{if .Story.ReadMins}}<span class="text-muted">· {{.Story.ReadMins}} min read</span>{{end}}
|
||||||
{{if .Story.Views}}<span class="ml-auto inline-flex items-center gap-1 text-[color:var(--ink)]/45 tabular-nums" title="{{.Story.Views}} reads">
|
{{if .Story.Views}}<span class="ml-auto inline-flex items-center gap-1 text-muted tabular-nums" title="{{.Story.Views}} reads">
|
||||||
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
||||||
{{.Story.Views}}</span>{{end}}
|
{{.Story.Views}}</span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
{{.Story.Headline}}
|
{{.Story.Headline}}
|
||||||
</h3>
|
</h3>
|
||||||
{{if .Story.Lede}}
|
{{if .Story.Lede}}
|
||||||
<p class="text-sm text-[color:var(--ink)]/70 line-clamp-3">{{.Story.Lede}}</p>
|
<p class="text-sm text-muted line-clamp-3">{{.Story.Lede}}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if .Story.Paywalled}}
|
{{if .Story.Paywalled}}
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
{{define "_chipbar"}}
|
{{define "_chipbar"}}
|
||||||
<section data-chipbar
|
<section data-chipbar
|
||||||
class="rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
class="rounded-3xl bg-card p-5 sm:p-6 shadow-pete border-2 border-ink/10">
|
||||||
<div class="flex flex-wrap items-center gap-x-6 gap-y-4">
|
<div class="flex flex-wrap items-center gap-x-6 gap-y-4">
|
||||||
|
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">Chips in front of you</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">Chips in front of you</div>
|
||||||
<div class="flex items-baseline gap-2">
|
<div class="flex items-baseline gap-2">
|
||||||
<span data-chips class="font-display text-3xl font-bold tabular-nums">—</span>
|
<span data-chips class="font-display text-3xl font-bold tabular-nums">—</span>
|
||||||
<span data-pending class="hidden text-sm font-semibold text-[color:var(--ink)]/60 animate-pulse"></span>
|
<span data-pending class="hidden text-sm font-semibold text-muted animate-pulse"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">Your wallet</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">Your wallet</div>
|
||||||
<div class="flex items-baseline gap-1.5">
|
<div class="flex items-baseline gap-1.5">
|
||||||
<span data-euros class="font-display text-2xl font-bold tabular-nums text-[color:var(--ink)]/70">—</span>
|
<span data-euros class="font-display text-2xl font-bold tabular-nums text-muted">—</span>
|
||||||
<span class="text-sm text-[color:var(--ink)]/40" title="Pete reads this off the game box every couple of minutes, so it lags a little.">€, give or take</span>
|
<span class="text-sm text-muted" title="Pete reads this off the game box every couple of minutes, so it lags a little.">€, give or take</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-auto flex flex-wrap items-center gap-2">
|
<div class="ml-auto flex flex-wrap items-center gap-2">
|
||||||
<div class="flex items-center gap-1.5 rounded-full bg-[color:var(--ink)]/5 p-1.5">
|
<div class="flex items-center gap-1.5 rounded-full bg-ink/5 p-1.5">
|
||||||
<label for="buyin-amount" class="sr-only">How many chips?</label>
|
<label for="buyin-amount" class="sr-only">How many chips?</label>
|
||||||
<input id="buyin-amount" data-buyin-amount type="number" min="1" step="1" value="100"
|
<input id="buyin-amount" data-buyin-amount type="number" min="1" step="1" value="100"
|
||||||
inputmode="numeric" enterkeyhint="go"
|
inputmode="numeric" enterkeyhint="go"
|
||||||
class="w-24 rounded-full bg-[color:var(--card)] px-3 py-1.5 text-sm font-semibold tabular-nums
|
class="w-24 rounded-full bg-card px-3 py-1.5 text-sm font-semibold tabular-nums
|
||||||
border-2 border-[color:var(--ink)]/10 focus:outline-none focus:border-[color:var(--accent)]">
|
border-2 border-ink/10 focus:outline-none focus:border-accent">
|
||||||
<button type="button" data-buyin
|
<button type="button" data-buyin
|
||||||
class="rounded-full bg-[color:var(--accent)] px-4 py-1.5 text-sm font-bold text-white shadow-pete
|
class="rounded-full bg-accent px-4 py-1.5 text-sm font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Buy chips
|
Buy chips
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" data-cashout
|
<button type="button" data-cashout
|
||||||
class="rounded-full bg-[color:var(--card)] px-4 py-2 text-sm font-bold shadow-pete border-2 border-[color:var(--ink)]/10
|
class="rounded-full bg-card px-4 py-2 text-sm font-bold shadow-pete border-2 border-ink/10
|
||||||
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:bg-ink/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Cash out
|
Cash out
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -42,8 +42,8 @@
|
|||||||
one thing every table has, and the sound is the room's rather than any
|
one thing every table has, and the sound is the room's rather than any
|
||||||
one game's. -->
|
one game's. -->
|
||||||
<button type="button" data-sfx-toggle aria-pressed="false"
|
<button type="button" data-sfx-toggle aria-pressed="false"
|
||||||
class="grid h-10 w-10 place-items-center rounded-full bg-[color:var(--card)] shadow-pete border-2 border-[color:var(--ink)]/10
|
class="grid h-10 w-10 place-items-center rounded-full bg-card shadow-pete border-2 border-ink/10
|
||||||
hover:bg-[color:var(--ink)]/5 active:translate-y-px transition">
|
hover:bg-ink/5 active:translate-y-px transition">
|
||||||
<svg data-sfx-on viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5" aria-hidden="true">
|
<svg data-sfx-on viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5" aria-hidden="true">
|
||||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
||||||
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>
|
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>
|
||||||
@@ -59,6 +59,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p data-chipbar-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
<p data-chipbar-msg class="hidden mt-3 rounded-2xl bg-ink/5 px-4 py-2 text-sm font-semibold"></p>
|
||||||
</section>
|
</section>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
off .Path. */}}
|
off .Path. */}}
|
||||||
{{define "realmnav"}}
|
{{define "realmnav"}}
|
||||||
<nav class="mb-5 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm">
|
<nav class="mb-5 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm">
|
||||||
<a href="/adventure" class="inline-flex items-center gap-1.5 font-semibold text-[color:var(--ink)]/60 hover:text-[color:var(--ink)] transition">
|
<a href="/adventure" class="inline-flex items-center gap-1.5 font-semibold text-muted hover:text-ink transition">
|
||||||
<span aria-hidden="true">←</span> All dispatches
|
<span aria-hidden="true">←</span> All dispatches
|
||||||
</a>
|
</a>
|
||||||
<span class="text-[color:var(--ink)]/20" aria-hidden="true">·</span>
|
<span class="text-ink/20" aria-hidden="true">·</span>
|
||||||
<a href="/adventure/realm" class="realm-tab{{if eq .Path "/adventure/realm"}} realm-tab-on{{end}}">The map</a>
|
<a href="/adventure/realm" class="realm-tab{{if eq .Path "/adventure/realm"}} realm-tab-on{{end}}">The map</a>
|
||||||
<a href="/adventure/standings" class="realm-tab{{if eq .Path "/adventure/standings"}} realm-tab-on{{end}}">The board</a>
|
<a href="/adventure/standings" class="realm-tab{{if eq .Path "/adventure/standings"}} realm-tab-on{{end}}">The board</a>
|
||||||
<a href="/adventure/firsts" class="realm-tab{{if eq .Path "/adventure/firsts"}} realm-tab-on{{end}}">Hall of firsts</a>
|
<a href="/adventure/firsts" class="realm-tab{{if eq .Path "/adventure/firsts"}} realm-tab-on{{end}}">Hall of firsts</a>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div class="flex items-center gap-3 min-w-0">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
<a href="/games" class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-[color:var(--card)] shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition" title="Back to the casino">
|
<a href="/games" class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-card shadow-pete border-2 border-ink/10 hover:bg-ink/5 transition" title="Back to the casino">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5" aria-hidden="true">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5" aria-hidden="true">
|
||||||
<path d="M19 12H5"></path><polyline points="12 19 5 12 12 5"></polyline>
|
<path d="M19 12H5"></path><polyline points="12 19 5 12 12 5"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<h1 class="font-display text-3xl font-bold">Blackjack</h1>
|
<h1 class="font-display text-3xl font-bold">Blackjack</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-[color:var(--ink)]/50">Six decks · pays 3:2 · dealer hits soft 17</p>
|
<p class="text-sm text-muted">Six decks · pays 3:2 · dealer hits soft 17</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{template "_chipbar" .}}
|
{{template "_chipbar" .}}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<!-- The felt. The two things on it that aren't cards are the shoe the cards
|
<!-- The felt. The two things on it that aren't cards are the shoe the cards
|
||||||
come out of and the rack the money comes out of — a chip on this table is
|
come out of and the rack the money comes out of — a chip on this table is
|
||||||
always travelling between one of those and the spot in front of you. -->
|
always travelling between one of those and the spot in front of you. -->
|
||||||
<section class="pete-felt relative overflow-hidden rounded-3xl p-6 sm:p-10 shadow-pete-lg border-2 border-[color:var(--ink)]/10">
|
<section class="pete-felt relative overflow-hidden rounded-3xl p-6 sm:p-10 shadow-pete-lg border-2 border-ink/10">
|
||||||
|
|
||||||
<div class="pete-rack" data-at="shoe" data-house aria-hidden="true">
|
<div class="pete-rack" data-at="shoe" data-house aria-hidden="true">
|
||||||
<span data-chip="500" style="--stack: 5"></span>
|
<span data-chip="500" style="--stack: 5"></span>
|
||||||
@@ -86,10 +86,10 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Betting: shown between hands. -->
|
<!-- Betting: shown between hands. -->
|
||||||
<section data-betting class="rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section data-betting class="rounded-3xl bg-card p-5 sm:p-6 shadow-pete border-2 border-ink/10">
|
||||||
<div class="flex flex-wrap items-center gap-x-6 gap-y-4">
|
<div class="flex flex-wrap items-center gap-x-6 gap-y-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">Your bet</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">Your bet</div>
|
||||||
<div class="font-display text-3xl font-bold tabular-nums"><span data-bet-amount>0</span></div>
|
<div class="font-display text-3xl font-bold tabular-nums"><span data-bet-amount>0</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -101,43 +101,43 @@
|
|||||||
</button>
|
</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
<button type="button" data-bet-clear
|
<button type="button" data-bet-clear
|
||||||
class="rounded-full px-3 py-2 text-sm font-semibold text-[color:var(--ink)]/50 hover:text-[color:var(--ink)] transition">Clear</button>
|
class="rounded-full px-3 py-2 text-sm font-semibold text-muted hover:text-ink transition">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" data-deal
|
<button type="button" data-deal
|
||||||
class="ml-auto rounded-full bg-[color:var(--accent)] px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
class="ml-auto rounded-full bg-accent px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Deal
|
Deal
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p data-table-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
<p data-table-msg class="hidden mt-3 rounded-2xl bg-ink/5 px-4 py-2 text-sm font-semibold"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Playing: shown while a hand is live. -->
|
<!-- Playing: shown while a hand is live. -->
|
||||||
<section data-actions class="hidden rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section data-actions class="hidden rounded-3xl bg-card p-5 sm:p-6 shadow-pete border-2 border-ink/10">
|
||||||
<div class="flex flex-wrap items-center justify-center gap-3">
|
<div class="flex flex-wrap items-center justify-center gap-3">
|
||||||
<button type="button" data-move="hit"
|
<button type="button" data-move="hit"
|
||||||
class="rounded-full bg-[color:var(--accent)] px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
class="rounded-full bg-accent px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Hit
|
Hit
|
||||||
</button>
|
</button>
|
||||||
<button type="button" data-move="stand"
|
<button type="button" data-move="stand"
|
||||||
class="rounded-full bg-[color:var(--card)] px-8 py-3 font-display text-lg font-bold shadow-pete border-2 border-[color:var(--ink)]/10
|
class="rounded-full bg-card px-8 py-3 font-display text-lg font-bold shadow-pete border-2 border-ink/10
|
||||||
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:bg-ink/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Stand
|
Stand
|
||||||
</button>
|
</button>
|
||||||
<button type="button" data-move="double"
|
<button type="button" data-move="double"
|
||||||
class="rounded-full bg-[color:var(--card)] px-8 py-3 font-display text-lg font-bold shadow-pete border-2 border-[color:var(--ink)]/10
|
class="rounded-full bg-card px-8 py-3 font-display text-lg font-bold shadow-pete border-2 border-ink/10
|
||||||
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:bg-ink/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Double
|
Double
|
||||||
</button>
|
</button>
|
||||||
<button type="button" data-move="split"
|
<button type="button" data-move="split"
|
||||||
class="rounded-full bg-[color:var(--card)] px-8 py-3 font-display text-lg font-bold shadow-pete border-2 border-[color:var(--ink)]/10
|
class="rounded-full bg-card px-8 py-3 font-display text-lg font-bold shadow-pete border-2 border-ink/10
|
||||||
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:bg-ink/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Split
|
Split
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 text-center text-xs text-[color:var(--ink)]/40">
|
<p class="mt-3 text-center text-xs text-muted">
|
||||||
<kbd>h</kbd> hit · <kbd>s</kbd> stand · <kbd>d</kbd> double · <kbd>p</kbd> split
|
<kbd>h</kbd> hit · <kbd>s</kbd> stand · <kbd>d</kbd> double · <kbd>p</kbd> split
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -146,8 +146,8 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "scripts"}}
|
{{define "scripts"}}
|
||||||
<script src="/static/js/casino-fx.js" defer></script>
|
<script src="{{asset "/static/js/casino-fx.js"}}" defer></script>
|
||||||
<script src="/static/js/casino-cards.js" defer></script>
|
<script src="{{asset "/static/js/casino-cards.js"}}" defer></script>
|
||||||
<script src="/static/js/games.js" defer></script>
|
<script src="{{asset "/static/js/games.js"}}" defer></script>
|
||||||
<script src="/static/js/blackjack.js" defer></script>
|
<script src="{{asset "/static/js/blackjack.js"}}" defer></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
<section class="mt-2 mb-8">
|
<section class="mt-2 mb-8">
|
||||||
<div class="rounded-3xl bg-[color:var(--accent)] text-[#1c1305] p-6 sm:p-10 shadow-pete relative overflow-hidden">
|
<div class="rounded-3xl bg-accent text-[#1c1305] p-6 sm:p-10 shadow-pete relative overflow-hidden">
|
||||||
<div class="absolute -top-6 -right-6 text-[12rem] opacity-20 select-none" aria-hidden="true">🔖</div>
|
<div class="absolute -top-6 -right-6 text-[12rem] opacity-20 select-none" aria-hidden="true">🔖</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<p class="text-sm uppercase tracking-[0.2em] opacity-70">saved for later</p>
|
<p class="text-sm uppercase tracking-[0.2em] opacity-70">saved for later</p>
|
||||||
@@ -22,15 +22,15 @@
|
|||||||
|
|
||||||
<nav class="mt-10 flex items-center justify-between">
|
<nav class="mt-10 flex items-center justify-between">
|
||||||
{{if .HasPrev}}
|
{{if .HasPrev}}
|
||||||
<a href="{{.PrevURL}}" class="rounded-full bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 px-5 py-2 font-semibold shadow-pete hover:-translate-y-0.5 transition">← newer</a>
|
<a href="{{.PrevURL}}" class="rounded-full bg-card border-2 border-ink/10 px-5 py-2 font-semibold shadow-pete hover:-translate-y-0.5 transition">← newer</a>
|
||||||
{{else}}<span></span>{{end}}
|
{{else}}<span></span>{{end}}
|
||||||
<span class="text-sm text-[color:var(--ink)]/60">page {{.Page}}</span>
|
<span class="text-sm text-muted">page {{.Page}}</span>
|
||||||
{{if .HasNext}}
|
{{if .HasNext}}
|
||||||
<a href="{{.NextURL}}" class="rounded-full bg-[color:var(--accent)] text-[#1c1305] px-5 py-2 font-semibold shadow-pete hover:-translate-y-0.5 transition">older →</a>
|
<a href="{{.NextURL}}" class="rounded-full bg-accent text-[#1c1305] px-5 py-2 font-semibold shadow-pete hover:-translate-y-0.5 transition">older →</a>
|
||||||
{{else}}<span></span>{{end}}
|
{{else}}<span></span>{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-dashed border-[color:var(--ink)]/15 p-10 text-center text-[color:var(--ink)]/60">
|
<div class="rounded-3xl bg-card border-2 border-dashed border-ink/15 p-10 text-center text-muted">
|
||||||
Nothing saved yet. Browse the feed and tap the 🔖 on a story to keep it here.
|
Nothing saved yet. Browse the feed and tap the 🔖 on a story to keep it here.
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
has happened to since their last visit. Above the Siege because everything
|
has happened to since their last visit. Above the Siege because everything
|
||||||
below this line is the realm's news and this is the reader's own. */}}
|
below this line is the realm's news and this is the reader's own. */}}
|
||||||
{{if .Away.Has}}
|
{{if .Away.Has}}
|
||||||
<section class="mb-6 rounded-3xl bg-[color:var(--card)] border-2 border-theme-adventure/30 p-5 sm:p-6 shadow-pete">
|
<section class="mb-6 rounded-3xl bg-card border-2 border-theme-adventure/30 p-5 sm:p-6 shadow-pete">
|
||||||
<div class="flex items-baseline justify-between gap-3 flex-wrap">
|
<div class="flex items-baseline justify-between gap-3 flex-wrap">
|
||||||
<h2 class="font-display text-xl font-bold">While you were away</h2>
|
<h2 class="font-display text-xl font-bold">While you were away</h2>
|
||||||
<span class="text-xs uppercase tracking-wider text-[color:var(--ink)]/45">{{.Away.Name}} · past {{.Away.Since}}</span>
|
<span class="text-xs uppercase tracking-wider text-muted">{{.Away.Name}} · past {{.Away.Since}}</span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="mt-3 space-y-2">
|
<ul class="mt-3 space-y-2">
|
||||||
{{range .Away.Lines}}
|
{{range .Away.Lines}}
|
||||||
@@ -31,9 +31,9 @@
|
|||||||
<span class="shrink-0" aria-hidden="true">{{.Emoji}}</span>
|
<span class="shrink-0" aria-hidden="true">{{.Emoji}}</span>
|
||||||
<span class="flex-1 text-sm">
|
<span class="flex-1 text-sm">
|
||||||
<span class="font-semibold group-hover:text-theme-adventure group-hover:underline">{{.Label}}</span>
|
<span class="font-semibold group-hover:text-theme-adventure group-hover:underline">{{.Label}}</span>
|
||||||
{{if .Line}}<span class="text-[color:var(--ink)]/60"> — {{.Line}}</span>{{end}}
|
{{if .Line}}<span class="text-muted"> — {{.Line}}</span>{{end}}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-xs text-[color:var(--ink)]/40 shrink-0 tabular-nums">{{.When}}</span>
|
<span class="text-xs text-muted shrink-0 tabular-nums">{{.When}}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -64,22 +64,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{{else if .Siege.History}}
|
{{else if .Siege.History}}
|
||||||
<a href="/adventure/siege" class="flex items-center gap-3 mb-6 rounded-2xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 px-5 py-3 shadow-pete hover:border-theme-adventure/40 transition">
|
<a href="/adventure/siege" class="flex items-center gap-3 mb-6 rounded-2xl bg-card border-2 border-ink/10 px-5 py-3 shadow-pete hover:border-theme-adventure/40 transition">
|
||||||
<span class="text-lg" aria-hidden="true">🏰</span>
|
<span class="text-lg" aria-hidden="true">🏰</span>
|
||||||
<span class="text-sm text-[color:var(--ink)]/70">Nothing camped outside town right now — <span class="font-semibold text-theme-adventure">the sieges we've fought</span></span>
|
<span class="text-sm text-muted">Nothing camped outside town right now — <span class="font-semibold text-theme-adventure">the sieges we've fought</span></span>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<section class="mb-10" id="roster" data-stale="{{.RosterStale}}">
|
<section class="mb-10" id="roster" data-stale="{{.RosterStale}}">
|
||||||
<div class="flex items-baseline justify-between mb-3">
|
<div class="flex items-baseline justify-between mb-3">
|
||||||
<h2 class="font-display text-2xl font-bold">Out there right now</h2>
|
<h2 class="font-display text-2xl font-bold">Out there right now</h2>
|
||||||
<p class="text-xs uppercase tracking-wider text-[color:var(--ink)]/50" id="roster-status">
|
<p class="text-xs uppercase tracking-wider text-muted" id="roster-status">
|
||||||
{{if .RosterStale}}last known — the wire's gone quiet{{else}}live{{end}}
|
{{if .RosterStale}}last known — the wire's gone quiet{{else}}live{{end}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 shadow-pete overflow-hidden {{if .RosterStale}}opacity-60{{end}}" id="roster-card">
|
<div class="rounded-3xl bg-card border-2 border-ink/10 shadow-pete overflow-hidden {{if .RosterStale}}opacity-60{{end}}" id="roster-card">
|
||||||
<ul class="divide-y divide-[color:var(--ink)]/10" id="roster-list">
|
<ul class="divide-y divide-ink/10" id="roster-list">
|
||||||
{{range .Roster}}
|
{{range .Roster}}
|
||||||
{{/* The layout is .roster-row, not utilities: this markup has a twin in the
|
{{/* The layout is .roster-row, not utilities: this markup has a twin in the
|
||||||
script below that re-renders the same list on every poll, and the two
|
script below that re-renders the same list on every poll, and the two
|
||||||
@@ -87,22 +87,22 @@
|
|||||||
<li class="roster-row" data-token="{{.Token}}" data-name="{{.Name}}">
|
<li class="roster-row" data-token="{{.Token}}" data-name="{{.Name}}">
|
||||||
<span class="roster-row-icon" aria-hidden="true">{{if .OnRun}}⚔{{else}}🏠{{end}}</span>
|
<span class="roster-row-icon" aria-hidden="true">{{if .OnRun}}⚔{{else}}🏠{{end}}</span>
|
||||||
<a href="/adventure/who/{{.Token}}" class="roster-row-name font-semibold truncate hover:text-theme-adventure hover:underline">{{.Name}}</a>
|
<a href="/adventure/who/{{.Token}}" class="roster-row-name font-semibold truncate hover:text-theme-adventure hover:underline">{{.Name}}</a>
|
||||||
<span class="roster-row-meta text-sm text-[color:var(--ink)]/60">lv {{.Level}} {{.ClassRace}}</span>
|
<span class="roster-row-meta text-sm text-muted">lv {{.Level}} {{.ClassRace}}</span>
|
||||||
<span class="roster-row-where text-sm {{if .OnRun}}font-semibold{{else}}text-[color:var(--ink)]/60{{end}}">
|
<span class="roster-row-where text-sm {{if .OnRun}}font-semibold{{else}}text-muted{{end}}">
|
||||||
{{.Where}}{{if .Idle}} <span class="text-[color:var(--ink)]/45">· {{.Idle}}</span>{{end}}
|
{{.Where}}{{if .Idle}} <span class="text-muted">· {{.Idle}}</span>{{end}}
|
||||||
</span>
|
</span>
|
||||||
{{if and $.User .OnRun}}
|
{{if and $.User .OnRun}}
|
||||||
<button type="button" class="mischief-send roster-row-act shrink-0 rounded-full bg-[color:var(--ink)]/5 hover:bg-red-500 hover:text-white border border-[color:var(--ink)]/15 px-3 py-1 text-xs font-semibold transition" data-token="{{.Token}}" data-name="{{.Name}}">send trouble</button>
|
<button type="button" class="mischief-send roster-row-act shrink-0 rounded-full bg-ink/5 hover:bg-red-500 hover:text-white border border-ink/15 px-3 py-1 text-xs font-semibold transition" data-token="{{.Token}}" data-name="{{.Name}}">send trouble</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
</li>
|
</li>
|
||||||
{{else}}
|
{{else}}
|
||||||
<li class="px-5 py-8 text-center text-[color:var(--ink)]/60">Nobody's out at the moment. Quiet week in the realm.</li>
|
<li class="px-5 py-8 text-center text-muted">Nobody's out at the moment. Quiet week in the realm.</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if not .User}}
|
{{if not .User}}
|
||||||
<p class="mt-3 text-xs text-[color:var(--ink)]/50">
|
<p class="mt-3 text-xs text-muted">
|
||||||
<a href="/auth/login?next={{.Path}}" class="underline hover:text-red-500">Sign in</a> to send something unpleasant their way.
|
<a href="/auth/login?next={{.Path}}" class="underline hover:text-red-500">Sign in</a> to send something unpleasant their way.
|
||||||
</p>
|
</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -111,30 +111,30 @@
|
|||||||
<!-- Your open contracts. Rendered from filed facts, never from live game state:
|
<!-- Your open contracts. Rendered from filed facts, never from live game state:
|
||||||
Pete only knows what gogobee told it happened. -->
|
Pete only knows what gogobee told it happened. -->
|
||||||
<div id="mischief-orders" class="mt-4 hidden">
|
<div id="mischief-orders" class="mt-4 hidden">
|
||||||
<h3 class="text-xs uppercase tracking-wider text-[color:var(--ink)]/50 mb-2">Trouble you've paid for</h3>
|
<h3 class="text-xs uppercase tracking-wider text-muted mb-2">Trouble you've paid for</h3>
|
||||||
<ul id="mischief-orders-list" class="space-y-2 text-sm"></ul>
|
<ul id="mischief-orders-list" class="space-y-2 text-sm"></ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- The buy dialog. Prices are gogobee's, pulled live from /api/mischief/catalog;
|
<!-- The buy dialog. Prices are gogobee's, pulled live from /api/mischief/catalog;
|
||||||
nothing here asserts a number of its own. -->
|
nothing here asserts a number of its own. -->
|
||||||
<div id="mischief-modal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/50 p-4">
|
<div id="mischief-modal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/50 p-4">
|
||||||
<div class="w-full max-w-md rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 shadow-pete overflow-hidden">
|
<div class="w-full max-w-md rounded-3xl bg-card border-2 border-ink/10 shadow-pete overflow-hidden">
|
||||||
<div class="flex items-center justify-between px-5 py-4 border-b border-[color:var(--ink)]/10">
|
<div class="flex items-center justify-between px-5 py-4 border-b border-ink/10">
|
||||||
<h3 class="font-display text-xl font-bold">Send trouble to <span id="mischief-target">—</span></h3>
|
<h3 class="font-display text-xl font-bold">Send trouble to <span id="mischief-target">—</span></h3>
|
||||||
<button type="button" id="mischief-close" class="text-2xl leading-none text-[color:var(--ink)]/50 hover:text-[color:var(--ink)]" aria-label="close">×</button>
|
<button type="button" id="mischief-close" class="text-2xl leading-none text-muted hover:text-ink" aria-label="close">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-5 py-4 space-y-3">
|
<div class="px-5 py-4 space-y-3">
|
||||||
<p class="text-sm text-[color:var(--ink)]/60">Pick how bad. They won't know who sent it — unless you sign it, or they live to read the receipt.</p>
|
<p class="text-sm text-muted">Pick how bad. They won't know who sent it — unless you sign it, or they live to read the receipt.</p>
|
||||||
<div id="mischief-tiers" class="space-y-2"></div>
|
<div id="mischief-tiers" class="space-y-2"></div>
|
||||||
<label class="flex items-center gap-2 text-sm">
|
<label class="flex items-center gap-2 text-sm">
|
||||||
<input type="checkbox" id="mischief-signed" class="rounded border-[color:var(--ink)]/30">
|
<input type="checkbox" id="mischief-signed" class="rounded border-ink/30">
|
||||||
<span>Sign my name to it <span class="text-[color:var(--ink)]/45">(+25%, and they'll know)</span></span>
|
<span>Sign my name to it <span class="text-muted">(+25%, and they'll know)</span></span>
|
||||||
</label>
|
</label>
|
||||||
<p id="mischief-balance" class="text-xs text-[color:var(--ink)]/50"></p>
|
<p id="mischief-balance" class="text-xs text-muted"></p>
|
||||||
<p id="mischief-result" class="text-sm hidden"></p>
|
<p id="mischief-result" class="text-sm hidden"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-end gap-2 px-5 py-4 border-t border-[color:var(--ink)]/10">
|
<div class="flex items-center justify-end gap-2 px-5 py-4 border-t border-ink/10">
|
||||||
<button type="button" id="mischief-cancel" class="rounded-full px-4 py-2 text-sm font-semibold text-[color:var(--ink)]/60 hover:text-[color:var(--ink)]">Never mind</button>
|
<button type="button" id="mischief-cancel" class="rounded-full px-4 py-2 text-sm font-semibold text-muted hover:text-ink">Never mind</button>
|
||||||
<button type="button" id="mischief-confirm" class="rounded-full bg-red-500 text-white px-5 py-2 text-sm font-semibold shadow-pete hover:-translate-y-0.5 transition disabled:opacity-40 disabled:translate-y-0" disabled>Put the word out</button>
|
<button type="button" id="mischief-confirm" class="rounded-full bg-red-500 text-white px-5 py-2 text-sm font-semibold shadow-pete hover:-translate-y-0.5 transition disabled:opacity-40 disabled:translate-y-0" disabled>Put the word out</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,17 +160,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function row(a) {
|
function row(a) {
|
||||||
var idle = a.Idle ? ' <span class="text-[color:var(--ink)]/45">· ' + esc(a.Idle) + '</span>' : '';
|
var idle = a.Idle ? ' <span class="text-muted">· ' + esc(a.Idle) + '</span>' : '';
|
||||||
// The server-rendered twin of this row is above, in the {{"{{range .Roster}}"}}
|
// The server-rendered twin of this row is above, in the {{"{{range .Roster}}"}}
|
||||||
// block. Keep the class names identical: the layout is all in .roster-row.
|
// block. Keep the class names identical: the layout is all in .roster-row.
|
||||||
var button = (signedIn && a.OnRun)
|
var button = (signedIn && a.OnRun)
|
||||||
? '<button type="button" class="mischief-send roster-row-act shrink-0 rounded-full bg-[color:var(--ink)]/5 hover:bg-red-500 hover:text-white border border-[color:var(--ink)]/15 px-3 py-1 text-xs font-semibold transition" data-token="' + esc(a.Token) + '" data-name="' + esc(a.Name) + '">send trouble</button>'
|
? '<button type="button" class="mischief-send roster-row-act shrink-0 rounded-full bg-ink/5 hover:bg-red-500 hover:text-white border border-ink/15 px-3 py-1 text-xs font-semibold transition" data-token="' + esc(a.Token) + '" data-name="' + esc(a.Name) + '">send trouble</button>'
|
||||||
: '';
|
: '';
|
||||||
return '<li class="roster-row" data-token="' + esc(a.Token) + '" data-name="' + esc(a.Name) + '">' +
|
return '<li class="roster-row" data-token="' + esc(a.Token) + '" data-name="' + esc(a.Name) + '">' +
|
||||||
'<span class="roster-row-icon" aria-hidden="true">' + (a.OnRun ? '⚔' : '🏠') + '</span>' +
|
'<span class="roster-row-icon" aria-hidden="true">' + (a.OnRun ? '⚔' : '🏠') + '</span>' +
|
||||||
'<a href="/adventure/who/' + esc(a.Token) + '" class="roster-row-name font-semibold truncate hover:text-theme-adventure hover:underline">' + esc(a.Name) + '</a>' +
|
'<a href="/adventure/who/' + esc(a.Token) + '" class="roster-row-name font-semibold truncate hover:text-theme-adventure hover:underline">' + esc(a.Name) + '</a>' +
|
||||||
'<span class="roster-row-meta text-sm text-[color:var(--ink)]/60">lv ' + esc(a.Level) + ' ' + esc(a.ClassRace) + '</span>' +
|
'<span class="roster-row-meta text-sm text-muted">lv ' + esc(a.Level) + ' ' + esc(a.ClassRace) + '</span>' +
|
||||||
'<span class="roster-row-where text-sm ' + (a.OnRun ? 'font-semibold' : 'text-[color:var(--ink)]/60') + '">' +
|
'<span class="roster-row-where text-sm ' + (a.OnRun ? 'font-semibold' : 'text-muted') + '">' +
|
||||||
esc(a.Where) + idle + '</span>' + button + '</li>';
|
esc(a.Where) + idle + '</span>' + button + '</li>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
var rows = data.adventurers || [];
|
var rows = data.adventurers || [];
|
||||||
list.innerHTML = rows.length
|
list.innerHTML = rows.length
|
||||||
? rows.map(row).join('')
|
? rows.map(row).join('')
|
||||||
: '<li class="px-5 py-8 text-center text-[color:var(--ink)]/60">Nobody\'s out at the moment. Quiet week in the realm.</li>';
|
: '<li class="px-5 py-8 text-center text-muted">Nobody\'s out at the moment. Quiet week in the realm.</li>';
|
||||||
status.textContent = data.stale ? "last known — the wire's gone quiet" : 'live';
|
status.textContent = data.stale ? "last known — the wire's gone quiet" : 'live';
|
||||||
card.classList.toggle('opacity-60', !!data.stale);
|
card.classList.toggle('opacity-60', !!data.stale);
|
||||||
})
|
})
|
||||||
@@ -226,7 +226,7 @@ function initMischief(list, esc) {
|
|||||||
|
|
||||||
function drawTiers() {
|
function drawTiers() {
|
||||||
if (!catalog || !catalog.tiers || !catalog.tiers.length) {
|
if (!catalog || !catalog.tiers || !catalog.tiers.length) {
|
||||||
tiersEl.innerHTML = '<p class="text-sm text-[color:var(--ink)]/50">The catalogue is between snapshots. Try again in a moment.</p>';
|
tiersEl.innerHTML = '<p class="text-sm text-muted">The catalogue is between snapshots. Try again in a moment.</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var signed = signedEl.checked;
|
var signed = signedEl.checked;
|
||||||
@@ -236,11 +236,11 @@ function initMischief(list, esc) {
|
|||||||
var price = signed ? t.signed_fee : t.fee;
|
var price = signed ? t.signed_fee : t.fee;
|
||||||
var tooDear = known && euro < price;
|
var tooDear = known && euro < price;
|
||||||
return '<button type="button" class="mischief-tier w-full text-left rounded-2xl border-2 px-4 py-2 transition ' +
|
return '<button type="button" class="mischief-tier w-full text-left rounded-2xl border-2 px-4 py-2 transition ' +
|
||||||
(chosen === t.key ? 'border-red-500 bg-red-500/10' : 'border-[color:var(--ink)]/10 hover:border-[color:var(--ink)]/25') +
|
(chosen === t.key ? 'border-red-500 bg-red-500/10' : 'border-ink/10 hover:border-ink/25') +
|
||||||
(tooDear ? ' opacity-40 cursor-not-allowed' : '') + '" data-key="' + esc(t.key) + '"' + (tooDear ? ' disabled' : '') + '>' +
|
(tooDear ? ' opacity-40 cursor-not-allowed' : '') + '" data-key="' + esc(t.key) + '"' + (tooDear ? ' disabled' : '') + '>' +
|
||||||
'<span class="flex items-baseline justify-between"><span class="font-semibold">' + esc(t.display) + '</span>' +
|
'<span class="flex items-baseline justify-between"><span class="font-semibold">' + esc(t.display) + '</span>' +
|
||||||
'<span class="font-semibold">' + euros(price) + '</span></span>' +
|
'<span class="font-semibold">' + euros(price) + '</span></span>' +
|
||||||
(t.blurb ? '<span class="block text-xs text-[color:var(--ink)]/55 mt-0.5">' + esc(t.blurb) + '</span>' : '') +
|
(t.blurb ? '<span class="block text-xs text-muted mt-0.5">' + esc(t.blurb) + '</span>' : '') +
|
||||||
'</button>';
|
'</button>';
|
||||||
}).join('');
|
}).join('');
|
||||||
balanceEl.textContent = known ? 'You have about ' + euros(euro) + ' to spend.' : '';
|
balanceEl.textContent = known ? 'You have about ' + euros(euro) + ' to spend.' : '';
|
||||||
@@ -275,10 +275,10 @@ function initMischief(list, esc) {
|
|||||||
var map = {
|
var map = {
|
||||||
pending: ['on its way', 'bg-amber-500/15 text-amber-700'],
|
pending: ['on its way', 'bg-amber-500/15 text-amber-700'],
|
||||||
placed: ['out for delivery', 'bg-red-500/15 text-red-600'],
|
placed: ['out for delivery', 'bg-red-500/15 text-red-600'],
|
||||||
bounced_funds: ["couldn't afford it", 'bg-[color:var(--ink)]/10 text-[color:var(--ink)]/60'],
|
bounced_funds: ["couldn't afford it", 'bg-ink/10 text-muted'],
|
||||||
bounced_ineligible: ['no longer a mark', 'bg-[color:var(--ink)]/10 text-[color:var(--ink)]/60']
|
bounced_ineligible: ['no longer a mark', 'bg-ink/10 text-muted']
|
||||||
};
|
};
|
||||||
var m = map[statusText] || [statusText, 'bg-[color:var(--ink)]/10 text-[color:var(--ink)]/60'];
|
var m = map[statusText] || [statusText, 'bg-ink/10 text-muted'];
|
||||||
return '<span class="rounded-full px-2 py-0.5 text-xs font-semibold ' + m[1] + '">' + esc(m[0]) + '</span>';
|
return '<span class="rounded-full px-2 py-0.5 text-xs font-semibold ' + m[1] + '">' + esc(m[0]) + '</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,10 +289,10 @@ function initMischief(list, esc) {
|
|||||||
if (!orders || !orders.length) { ordersWrap.classList.add('hidden'); return; }
|
if (!orders || !orders.length) { ordersWrap.classList.add('hidden'); return; }
|
||||||
ordersWrap.classList.remove('hidden');
|
ordersWrap.classList.remove('hidden');
|
||||||
ordersList.innerHTML = orders.map(function (o) {
|
ordersList.innerHTML = orders.map(function (o) {
|
||||||
var detail = o.detail ? ' <span class="text-[color:var(--ink)]/45">· ' + esc(o.detail) + '</span>' : '';
|
var detail = o.detail ? ' <span class="text-muted">· ' + esc(o.detail) + '</span>' : '';
|
||||||
return '<li class="flex items-center gap-3">' +
|
return '<li class="flex items-center gap-3">' +
|
||||||
'<span class="font-semibold">' + esc(o.target_name) + '</span>' +
|
'<span class="font-semibold">' + esc(o.target_name) + '</span>' +
|
||||||
'<span class="text-[color:var(--ink)]/55">' + esc(o.tier) + (o.signed ? ', signed' : '') + '</span>' +
|
'<span class="text-muted">' + esc(o.tier) + (o.signed ? ', signed' : '') + '</span>' +
|
||||||
'<span class="ml-auto">' + badge(o.status) + '</span>' + detail + '</li>';
|
'<span class="ml-auto">' + badge(o.status) + '</span>' + detail + '</li>';
|
||||||
}).join('');
|
}).join('');
|
||||||
})
|
})
|
||||||
@@ -328,14 +328,14 @@ function initMischief(list, esc) {
|
|||||||
loadOrders();
|
loadOrders();
|
||||||
setTimeout(close, 1200);
|
setTimeout(close, 1200);
|
||||||
} else {
|
} else {
|
||||||
resultEl.className = 'text-sm text-[color:var(--ink)]/70';
|
resultEl.className = 'text-sm text-muted';
|
||||||
resultEl.textContent = (res.body && res.body.error) || 'That didn\'t take. Try again.';
|
resultEl.textContent = (res.body && res.body.error) || 'That didn\'t take. Try again.';
|
||||||
confirmEl.disabled = false;
|
confirmEl.disabled = false;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
resultEl.classList.remove('hidden');
|
resultEl.classList.remove('hidden');
|
||||||
resultEl.className = 'text-sm text-[color:var(--ink)]/70';
|
resultEl.className = 'text-sm text-muted';
|
||||||
resultEl.textContent = 'The line dropped. Try again.';
|
resultEl.textContent = 'The line dropped. Try again.';
|
||||||
confirmEl.disabled = false;
|
confirmEl.disabled = false;
|
||||||
});
|
});
|
||||||
@@ -356,15 +356,15 @@ function initMischief(list, esc) {
|
|||||||
|
|
||||||
<nav class="mt-10 flex items-center justify-between">
|
<nav class="mt-10 flex items-center justify-between">
|
||||||
{{if .HasPrev}}
|
{{if .HasPrev}}
|
||||||
<a href="{{.PrevURL}}" class="rounded-full bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 px-5 py-2 font-semibold shadow-pete hover:-translate-y-0.5 transition">← newer</a>
|
<a href="{{.PrevURL}}" class="rounded-full bg-card border-2 border-ink/10 px-5 py-2 font-semibold shadow-pete hover:-translate-y-0.5 transition">← newer</a>
|
||||||
{{else}}<span></span>{{end}}
|
{{else}}<span></span>{{end}}
|
||||||
<span class="text-sm text-[color:var(--ink)]/60">page {{.Page}}</span>
|
<span class="text-sm text-muted">page {{.Page}}</span>
|
||||||
{{if .HasNext}}
|
{{if .HasNext}}
|
||||||
<a href="{{.NextURL}}" class="rounded-full bg-theme-{{.Channel.Theme}} text-white px-5 py-2 font-semibold shadow-pete hover:-translate-y-0.5 transition">older →</a>
|
<a href="{{.NextURL}}" class="rounded-full bg-theme-{{.Channel.Theme}} text-white px-5 py-2 font-semibold shadow-pete hover:-translate-y-0.5 transition">older →</a>
|
||||||
{{else}}<span></span>{{end}}
|
{{else}}<span></span>{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-dashed border-[color:var(--ink)]/15 p-10 text-center text-[color:var(--ink)]/60">
|
<div class="rounded-3xl bg-card border-2 border-dashed border-ink/15 p-10 text-center text-muted">
|
||||||
Pete hasn't catalogued anything for {{.Channel.Title}} yet.
|
Pete hasn't catalogued anything for {{.Channel.Title}} yet.
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -39,15 +39,15 @@
|
|||||||
{{range .Firsts}}
|
{{range .Firsts}}
|
||||||
<li class="firsts-entry firsts-entry-{{.Kind}}">
|
<li class="firsts-entry firsts-entry-{{.Kind}}">
|
||||||
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
||||||
<span class="text-[11px] uppercase tracking-wider text-[color:var(--ink)]/40 font-semibold">{{.Label}}</span>
|
<span class="text-[11px] uppercase tracking-wider text-muted font-semibold">{{.Label}}</span>
|
||||||
<span class="font-display font-bold text-base">{{.Display}}</span>
|
<span class="font-display font-bold text-base">{{.Display}}</span>
|
||||||
{{if .Tier}}<span class="text-[11px] text-[color:var(--ink)]/35 tabular-nums">T{{.Tier}}</span>{{end}}
|
{{if .Tier}}<span class="text-[11px] text-muted tabular-nums">T{{.Tier}}</span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-0.5 text-sm text-[color:var(--ink)]/60">
|
<p class="mt-0.5 text-sm text-muted">
|
||||||
{{if .Token}}<a href="/adventure/who/{{.Token}}" class="font-semibold text-[color:var(--ink)]/80 hover:text-theme-adventure transition">{{.Holder}}</a>
|
{{if .Token}}<a href="/adventure/who/{{.Token}}" class="font-semibold text-ink/80 hover:text-theme-adventure transition">{{.Holder}}</a>
|
||||||
{{else if .Holder}}<span class="font-semibold text-[color:var(--ink)]/80">{{.Holder}}</span>
|
{{else if .Holder}}<span class="font-semibold text-ink/80">{{.Holder}}</span>
|
||||||
{{else}}<span class="italic text-[color:var(--ink)]/40">nobody left who'll admit to it</span>{{end}}
|
{{else}}<span class="italic text-muted">nobody left who'll admit to it</span>{{end}}
|
||||||
{{if .When}}<span class="text-[color:var(--ink)]/35"> · {{.When}}</span>{{end}}
|
{{if .When}}<span class="text-muted"> · {{.When}}</span>{{end}}
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -55,15 +55,15 @@
|
|||||||
</section>
|
</section>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<p class="mt-8 text-xs text-[color:var(--ink)]/40 text-center max-w-xl mx-auto">
|
<p class="mt-8 text-xs text-muted text-center max-w-xl mx-auto">
|
||||||
An entry with no name is one where the record has outlived the record-holder —
|
An entry with no name is one where the record has outlived the record-holder —
|
||||||
a thing found and long since given away, or somebody who'd rather I didn't say.
|
a thing found and long since given away, or somebody who'd rather I didn't say.
|
||||||
The claim still stands.
|
The claim still stands.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="mt-6 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
<div class="mt-6 rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete">
|
||||||
<p class="text-sm text-[color:var(--ink)]/60">
|
<p class="text-sm text-muted">
|
||||||
{{if .Firsts.Known}}
|
{{if .Firsts.Known}}
|
||||||
Nothing's happened for the first time yet. Everything is about to be a first.
|
Nothing's happened for the first time yet. Everything is about to be a first.
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
<section class="mt-2 mb-8">
|
<section class="mt-2 mb-8">
|
||||||
<div class="rounded-3xl bg-[color:var(--accent)] text-[#1c1305] p-6 sm:p-10 shadow-pete relative overflow-hidden">
|
<div class="rounded-3xl bg-accent text-[#1c1305] p-6 sm:p-10 shadow-pete relative overflow-hidden">
|
||||||
<div class="absolute -top-6 -right-6 text-[12rem] opacity-20 select-none" aria-hidden="true">✨</div>
|
<div class="absolute -top-6 -right-6 text-[12rem] opacity-20 select-none" aria-hidden="true">✨</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<p class="text-sm uppercase tracking-[0.2em] opacity-70">picked for you</p>
|
<p class="text-sm uppercase tracking-[0.2em] opacity-70">picked for you</p>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-dashed border-[color:var(--ink)]/15 p-10 text-center text-[color:var(--ink)]/60">
|
<div class="rounded-3xl bg-card border-2 border-dashed border-ink/15 p-10 text-center text-muted">
|
||||||
Nothing to tune yet. Read a few stories or bookmark the ones you like, and Pete will start building a feed around them.
|
Nothing to tune yet. Read a few stories or bookmark the ones you like, and Pete will start building a feed around them.
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
|
|
||||||
<section class="rounded-3xl bg-[color:var(--card)] p-6 sm:p-8 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section class="rounded-3xl bg-card p-6 sm:p-8 shadow-pete border-2 border-ink/10">
|
||||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h1 class="font-display text-3xl sm:text-4xl font-bold">Welcome in 🎲</h1>
|
<h1 class="font-display text-3xl sm:text-4xl font-bold">Welcome in 🎲</h1>
|
||||||
<p class="mt-2 max-w-xl text-[color:var(--ink)]/70">
|
<p class="mt-2 max-w-xl text-muted">
|
||||||
Real euros, from the same wallet as everything else. Chips are one euro each,
|
Real euros, from the same wallet as everything else. Chips are one euro each,
|
||||||
and whatever you don't spend goes straight back when you cash out.
|
and whatever you don't spend goes straight back when you cash out.
|
||||||
</p>
|
</p>
|
||||||
@@ -27,80 +27,80 @@
|
|||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
|
|
||||||
<a href="/games/blackjack"
|
<a href="/games/blackjack"
|
||||||
class="group rounded-3xl bg-[color:var(--card)] p-6 shadow-pete border-2 border-[color:var(--ink)]/10 hover:-translate-y-0.5 hover:shadow-pete-lg transition">
|
class="group rounded-3xl bg-card p-6 shadow-pete border-2 border-ink/10 hover:-translate-y-0.5 hover:shadow-pete-lg transition">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-[color:var(--accent)]/25 text-2xl">🃏</span>
|
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-accent/25 text-2xl">🃏</span>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h3 class="font-display text-xl font-bold">Blackjack</h3>
|
<h3 class="font-display text-xl font-bold">Blackjack</h3>
|
||||||
<p class="text-sm text-[color:var(--ink)]/60">Six decks, blackjack pays 3:2.</p>
|
<p class="text-sm text-muted">Six decks, blackjack pays 3:2.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="ml-auto shrink-0 rounded-full bg-theme-gaming px-3 py-1 text-xs font-bold uppercase tracking-wider text-white">Open</span>
|
<span class="ml-auto shrink-0 rounded-full bg-theme-gaming px-3 py-1 text-xs font-bold uppercase tracking-wider text-white">Open</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-4 text-sm text-[color:var(--ink)]/70">
|
<p class="mt-4 text-sm text-muted">
|
||||||
The dealer stands on 17 and hits a soft one. House takes {{.RakePct}}% of what you win,
|
The dealer stands on 17 and hits a soft one. House takes {{.RakePct}}% of what you win,
|
||||||
and nothing at all when you lose or push.
|
and nothing at all when you lose or push.
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/games/hangman"
|
<a href="/games/hangman"
|
||||||
class="group rounded-3xl bg-[color:var(--card)] p-6 shadow-pete border-2 border-[color:var(--ink)]/10 hover:-translate-y-0.5 hover:shadow-pete-lg transition">
|
class="group rounded-3xl bg-card p-6 shadow-pete border-2 border-ink/10 hover:-translate-y-0.5 hover:shadow-pete-lg transition">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-[color:var(--accent)]/25 text-2xl">🪢</span>
|
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-accent/25 text-2xl">🪢</span>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h3 class="font-display text-xl font-bold">Hangman</h3>
|
<h3 class="font-display text-xl font-bold">Hangman</h3>
|
||||||
<p class="text-sm text-[color:var(--ink)]/60">Guess the phrase, keep the multiple.</p>
|
<p class="text-sm text-muted">Guess the phrase, keep the multiple.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="ml-auto shrink-0 rounded-full bg-theme-gaming px-3 py-1 text-xs font-bold uppercase tracking-wider text-white">Open</span>
|
<span class="ml-auto shrink-0 rounded-full bg-theme-gaming px-3 py-1 text-xs font-bold uppercase tracking-wider text-white">Open</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-4 text-sm text-[color:var(--ink)]/70">
|
<p class="mt-4 text-sm text-muted">
|
||||||
Short phrases pay up to 2.6×. You get {{.MaxWrong}} lives, and every wrong guess
|
Short phrases pay up to 2.6×. You get {{.MaxWrong}} lives, and every wrong guess
|
||||||
takes a tenth off what a win is worth.
|
takes a tenth off what a win is worth.
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/games/solitaire"
|
<a href="/games/solitaire"
|
||||||
class="group rounded-3xl bg-[color:var(--card)] p-6 shadow-pete border-2 border-[color:var(--ink)]/10 hover:-translate-y-0.5 hover:shadow-pete-lg transition">
|
class="group rounded-3xl bg-card p-6 shadow-pete border-2 border-ink/10 hover:-translate-y-0.5 hover:shadow-pete-lg transition">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-[color:var(--accent)]/25 text-2xl">🂡</span>
|
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-accent/25 text-2xl">🂡</span>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h3 class="font-display text-xl font-bold">Solitaire</h3>
|
<h3 class="font-display text-xl font-bold">Solitaire</h3>
|
||||||
<p class="text-sm text-[color:var(--ink)]/60">Buy the deck, win it back a card at a time.</p>
|
<p class="text-sm text-muted">Buy the deck, win it back a card at a time.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="ml-auto shrink-0 rounded-full bg-theme-gaming px-3 py-1 text-xs font-bold uppercase tracking-wider text-white">Open</span>
|
<span class="ml-auto shrink-0 rounded-full bg-theme-gaming px-3 py-1 text-xs font-bold uppercase tracking-wider text-white">Open</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-4 text-sm text-[color:var(--ink)]/70">
|
<p class="mt-4 text-sm text-muted">
|
||||||
Vegas rules. Your stake buys the deck and doesn't come back — every card you
|
Vegas rules. Your stake buys the deck and doesn't come back — every card you
|
||||||
get home pays a slice of it in. Cash the board whenever you like.
|
get home pays a slice of it in. Cash the board whenever you like.
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/games/trivia"
|
<a href="/games/trivia"
|
||||||
class="group rounded-3xl bg-[color:var(--card)] p-6 shadow-pete border-2 border-[color:var(--ink)]/10 hover:-translate-y-0.5 hover:shadow-pete-lg transition">
|
class="group rounded-3xl bg-card p-6 shadow-pete border-2 border-ink/10 hover:-translate-y-0.5 hover:shadow-pete-lg transition">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-[color:var(--accent)]/25 text-2xl">🧠</span>
|
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-accent/25 text-2xl">🧠</span>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h3 class="font-display text-xl font-bold">Trivia</h3>
|
<h3 class="font-display text-xl font-bold">Trivia</h3>
|
||||||
<p class="text-sm text-[color:var(--ink)]/60">Climb the ladder, or take the money.</p>
|
<p class="text-sm text-muted">Climb the ladder, or take the money.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="ml-auto shrink-0 rounded-full bg-theme-gaming px-3 py-1 text-xs font-bold uppercase tracking-wider text-white">Open</span>
|
<span class="ml-auto shrink-0 rounded-full bg-theme-gaming px-3 py-1 text-xs font-bold uppercase tracking-wider text-white">Open</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-4 text-sm text-[color:var(--ink)]/70">
|
<p class="mt-4 text-sm text-muted">
|
||||||
{{.Rungs}} questions, and every right answer multiplies what you're holding. A wrong
|
{{.Rungs}} questions, and every right answer multiplies what you're holding. A wrong
|
||||||
one loses the lot. Answer fast: the multiple decays as the clock runs.
|
one loses the lot. Answer fast: the multiple decays as the clock runs.
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/games/uno"
|
<a href="/games/uno"
|
||||||
class="group rounded-3xl bg-[color:var(--card)] p-6 shadow-pete border-2 border-[color:var(--ink)]/10 hover:-translate-y-0.5 hover:shadow-pete-lg transition">
|
class="group rounded-3xl bg-card p-6 shadow-pete border-2 border-ink/10 hover:-translate-y-0.5 hover:shadow-pete-lg transition">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-[color:var(--accent)]/25 text-2xl">🎴</span>
|
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-accent/25 text-2xl">🎴</span>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h3 class="font-display text-xl font-bold">UNO</h3>
|
<h3 class="font-display text-xl font-bold">UNO</h3>
|
||||||
<p class="text-sm text-[color:var(--ink)]/60">Go out first, take the table.</p>
|
<p class="text-sm text-muted">Go out first, take the table.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="ml-auto shrink-0 rounded-full bg-theme-gaming px-3 py-1 text-xs font-bold uppercase tracking-wider text-white">Open</span>
|
<span class="ml-auto shrink-0 rounded-full bg-theme-gaming px-3 py-1 text-xs font-bold uppercase tracking-wider text-white">Open</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-4 text-sm text-[color:var(--ink)]/70">
|
<p class="mt-4 text-sm text-muted">
|
||||||
One to three bots, and the more of them there are the more it pays: up to 4.1× for
|
One to three bots, and the more of them there are the more it pays: up to 4.1× for
|
||||||
beating a full table. Anybody else going out first takes your stake. Or throw the
|
beating a full table. Anybody else going out first takes your stake. Or throw the
|
||||||
No Mercy switch: 168 cards, draws that stack, and twenty-five in your hand puts you out.
|
No Mercy switch: 168 cards, draws that stack, and twenty-five in your hand puts you out.
|
||||||
@@ -108,30 +108,30 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/games/holdem"
|
<a href="/games/holdem"
|
||||||
class="group rounded-3xl bg-[color:var(--card)] p-6 shadow-pete border-2 border-[color:var(--ink)]/10 hover:-translate-y-0.5 hover:shadow-pete-lg transition">
|
class="group rounded-3xl bg-card p-6 shadow-pete border-2 border-ink/10 hover:-translate-y-0.5 hover:shadow-pete-lg transition">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-[color:var(--accent)]/25 text-2xl">♠️</span>
|
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-accent/25 text-2xl">♠️</span>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h3 class="font-display text-xl font-bold">Texas Hold'em</h3>
|
<h3 class="font-display text-xl font-bold">Texas Hold'em</h3>
|
||||||
<p class="text-sm text-[color:var(--ink)]/60">Buy in. Beat them. Get up.</p>
|
<p class="text-sm text-muted">Buy in. Beat them. Get up.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="ml-auto shrink-0 rounded-full bg-theme-gaming px-3 py-1 text-xs font-bold uppercase tracking-wider text-white">Open</span>
|
<span class="ml-auto shrink-0 rounded-full bg-theme-gaming px-3 py-1 text-xs font-bold uppercase tracking-wider text-white">Open</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-4 text-sm text-[color:var(--ink)]/70">
|
<p class="mt-4 text-sm text-muted">
|
||||||
A real cash game against bots that were trained on it, not scripted. No multiple, no
|
A real cash game against bots that were trained on it, not scripted. No multiple, no
|
||||||
3:2 — you leave with whatever is in front of you, less the rake on the pots you win.
|
3:2 — you leave with whatever is in front of you, less the rake on the pots you win.
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{{range .Soon}}
|
{{range .Soon}}
|
||||||
<div class="rounded-3xl bg-[color:var(--card)]/60 p-6 shadow-pete border-2 border-dashed border-[color:var(--ink)]/15">
|
<div class="rounded-3xl bg-card/60 p-6 shadow-pete border-2 border-dashed border-ink/15">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-[color:var(--ink)]/5 text-2xl grayscale">{{.Emoji}}</span>
|
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-ink/5 text-2xl grayscale">{{.Emoji}}</span>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h3 class="font-display text-xl font-bold text-[color:var(--ink)]/50">{{.Name}}</h3>
|
<h3 class="font-display text-xl font-bold text-muted">{{.Name}}</h3>
|
||||||
<p class="text-sm text-[color:var(--ink)]/40">{{.Blurb}}</p>
|
<p class="text-sm text-muted">{{.Blurb}}</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="ml-auto shrink-0 rounded-full border-2 border-[color:var(--ink)]/10 px-3 py-1 text-xs font-bold uppercase tracking-wider text-[color:var(--ink)]/40">Soon</span>
|
<span class="ml-auto shrink-0 rounded-full border-2 border-ink/10 px-3 py-1 text-xs font-bold uppercase tracking-wider text-muted">Soon</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -139,9 +139,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="rounded-3xl bg-[color:var(--card)] p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section class="rounded-3xl bg-card p-6 shadow-pete border-2 border-ink/10">
|
||||||
<h2 class="font-display text-xl font-bold mb-2">House rules</h2>
|
<h2 class="font-display text-xl font-bold mb-2">House rules</h2>
|
||||||
<ul class="space-y-1.5 text-sm text-[color:var(--ink)]/70">
|
<ul class="space-y-1.5 text-sm text-muted">
|
||||||
<li>· A chip is a euro. Nothing is worth more here than it is out there.</li>
|
<li>· A chip is a euro. Nothing is worth more here than it is out there.</li>
|
||||||
<li>· You can have {{.Cap}} chips in front of you at most. That's the limit for one sitting.</li>
|
<li>· You can have {{.Cap}} chips in front of you at most. That's the limit for one sitting.</li>
|
||||||
<li>· The house takes {{.RakePct}}% of your winnings. Never your stake: a push hands your bet straight back.</li>
|
<li>· The house takes {{.RakePct}}% of your winnings. Never your stake: a push hands your bet straight back.</li>
|
||||||
@@ -153,4 +153,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "scripts"}}<script src="/static/js/games.js" defer></script>{{end}}
|
{{define "scripts"}}<script src="{{asset "/static/js/games.js"}}" defer></script>{{end}}
|
||||||
|
|||||||
@@ -10,16 +10,16 @@
|
|||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
|
|
||||||
<section class="rounded-3xl bg-[color:var(--card)] p-6 sm:p-10 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section class="rounded-3xl bg-card p-6 sm:p-10 shadow-pete border-2 border-ink/10">
|
||||||
{{/* No decoration in here on purpose. The chip stacks want the casino's JS,
|
{{/* No decoration in here on purpose. The chip stacks want the casino's JS,
|
||||||
which the door doesn't load, and an emoji big enough to balance the hero
|
which the door doesn't load, and an emoji big enough to balance the hero
|
||||||
is an emoji that isn't on every machine. The share card carries the
|
is an emoji that isn't on every machine. The share card carries the
|
||||||
pictures; the door carries the way in. */}}
|
pictures; the door carries the way in. */}}
|
||||||
<div class="flex flex-wrap items-center justify-between gap-6">
|
<div class="flex flex-wrap items-center justify-between gap-6">
|
||||||
<div class="min-w-0 max-w-2xl">
|
<div class="min-w-0 max-w-2xl">
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-[color:var(--accent)]">The doors are open</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-accent">The doors are open</p>
|
||||||
<h1 class="mt-2 font-display text-3xl sm:text-5xl font-bold">{{.Room.Name}}</h1>
|
<h1 class="mt-2 font-display text-3xl sm:text-5xl font-bold">{{.Room.Name}}</h1>
|
||||||
<p class="mt-3 text-[color:var(--ink)]/70">
|
<p class="mt-3 text-muted">
|
||||||
Six tables, played against the house and its bots, for the same euros you
|
Six tables, played against the house and its bots, for the same euros you
|
||||||
already have. A chip is worth a euro, the house takes {{.RakePct}}% of what you
|
already have. A chip is worth a euro, the house takes {{.RakePct}}% of what you
|
||||||
win and nothing at all when you lose, and whatever you don't spend comes
|
win and nothing at all when you lose, and whatever you don't spend comes
|
||||||
@@ -27,10 +27,10 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="mt-6 flex flex-wrap items-center gap-3">
|
<div class="mt-6 flex flex-wrap items-center gap-3">
|
||||||
<a href="/auth/login?next=/games"
|
<a href="/auth/login?next=/games"
|
||||||
class="inline-flex items-center gap-2 rounded-full bg-[color:var(--accent)] px-6 py-3 font-display text-lg font-bold text-[#20180c] shadow-pete hover:-translate-y-0.5 hover:shadow-pete-lg transition">
|
class="inline-flex items-center gap-2 rounded-full bg-accent px-6 py-3 font-display text-lg font-bold text-[#20180c] shadow-pete hover:-translate-y-0.5 hover:shadow-pete-lg transition">
|
||||||
Sign in and sit down
|
Sign in and sit down
|
||||||
</a>
|
</a>
|
||||||
<span class="text-sm text-[color:var(--ink)]/45">You'll need a parodia.dev account.</span>
|
<span class="text-sm text-muted">You'll need a parodia.dev account.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<p class="pb-2 text-center text-sm text-[color:var(--ink)]/40">
|
<p class="pb-2 text-center text-sm text-muted">
|
||||||
{{if eq .Room.Slug "casinopolis"}}The lights come on at six, and the place changes its name.
|
{{if eq .Room.Slug "casinopolis"}}The lights come on at six, and the place changes its name.
|
||||||
{{else}}It goes back to being Casinopolis at six in the morning.{{end}}
|
{{else}}It goes back to being Casinopolis at six in the morning.{{end}}
|
||||||
</p>
|
</p>
|
||||||
@@ -58,12 +58,12 @@
|
|||||||
|
|
||||||
{{define "_door_table"}}
|
{{define "_door_table"}}
|
||||||
<a href="{{.Href}}"
|
<a href="{{.Href}}"
|
||||||
class="group rounded-3xl bg-[color:var(--card)] p-6 shadow-pete border-2 border-[color:var(--ink)]/10 hover:-translate-y-0.5 hover:shadow-pete-lg transition">
|
class="group rounded-3xl bg-card p-6 shadow-pete border-2 border-ink/10 hover:-translate-y-0.5 hover:shadow-pete-lg transition">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-[color:var(--accent)]/25 text-2xl">{{.Emoji}}</span>
|
<span class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-accent/25 text-2xl">{{.Emoji}}</span>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h3 class="font-display text-xl font-bold">{{.Name}}</h3>
|
<h3 class="font-display text-xl font-bold">{{.Name}}</h3>
|
||||||
<p class="text-sm text-[color:var(--ink)]/60">{{.Line}}</p>
|
<p class="text-sm text-muted">{{.Line}}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/css/output.css">
|
<link rel="stylesheet" href="{{asset "/static/css/output.css"}}">
|
||||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpolygon points='16,3 27,9.5 27,22.5 16,29 5,22.5 5,9.5' fill='%23f2b53d' stroke='%232b2118' stroke-width='2.5' stroke-linejoin='round'/%3E%3C/svg%3E" type="image/svg+xml">
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpolygon points='16,3 27,9.5 27,22.5 16,29 5,22.5 5,9.5' fill='%23f2b53d' stroke='%232b2118' stroke-width='2.5' stroke-linejoin='round'/%3E%3C/svg%3E" type="image/svg+xml">
|
||||||
<meta name="theme-color" content="#17231d">
|
<meta name="theme-color" content="#17231d">
|
||||||
<script>
|
<script>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen bg-[color:var(--bg)] text-[color:var(--ink)]">
|
<body class="min-h-screen bg-[color:var(--bg)] text-ink">
|
||||||
<div class="pointer-events-none fixed inset-0 -z-10 cs-room" aria-hidden="true"></div>
|
<div class="pointer-events-none fixed inset-0 -z-10 cs-room" aria-hidden="true"></div>
|
||||||
|
|
||||||
<header class="mx-auto max-w-5xl px-4 pt-6 pb-4 sm:pt-10">
|
<header class="mx-auto max-w-5xl px-4 pt-6 pb-4 sm:pt-10">
|
||||||
@@ -73,14 +73,14 @@
|
|||||||
|
|
||||||
{{if .User}}
|
{{if .User}}
|
||||||
<a href="/auth/logout" title="Signed in as {{.User.Display}} — sign out"
|
<a href="/auth/logout" title="Signed in as {{.User.Display}} — sign out"
|
||||||
class="inline-flex shrink-0 items-center gap-2 rounded-full bg-[color:var(--card)] px-2.5 py-1.5 text-sm font-semibold shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
|
class="inline-flex shrink-0 items-center gap-2 rounded-full bg-card px-2.5 py-1.5 text-sm font-semibold shadow-pete border-2 border-ink/10 hover:bg-ink/5 transition">
|
||||||
<span class="grid h-6 w-6 place-items-center rounded-full bg-[color:var(--accent)] text-xs font-bold text-[#20180c]">{{.User.Initial}}</span>
|
<span class="grid h-6 w-6 place-items-center rounded-full bg-accent text-xs font-bold text-[#20180c]">{{.User.Initial}}</span>
|
||||||
<span class="hidden sm:inline max-w-[7rem] truncate">{{.User.Display}}</span>
|
<span class="hidden sm:inline max-w-[7rem] truncate">{{.User.Display}}</span>
|
||||||
</a>
|
</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{/* Nobody is signed in, which on any page but the front door can't happen. */}}
|
{{/* Nobody is signed in, which on any page but the front door can't happen. */}}
|
||||||
<a href="/auth/login?next=/games"
|
<a href="/auth/login?next=/games"
|
||||||
class="inline-flex shrink-0 items-center rounded-full bg-[color:var(--accent)] px-4 py-1.5 text-sm font-bold text-[#20180c] shadow-pete hover:-translate-y-0.5 transition">
|
class="inline-flex shrink-0 items-center rounded-full bg-accent px-4 py-1.5 text-sm font-bold text-[#20180c] shadow-pete hover:-translate-y-0.5 transition">
|
||||||
Sign in
|
Sign in
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
{{block "main" .}}{{end}}
|
{{block "main" .}}{{end}}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="mx-auto max-w-5xl px-4 pb-10 text-center text-xs text-[color:var(--ink)]/40">
|
<footer class="mx-auto max-w-5xl px-4 pb-10 text-center text-xs text-muted">
|
||||||
Play for what you can lose. Cash out whenever you like.
|
Play for what you can lose. Cash out whenever you like.
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
wants it and none of them owns it, and because it has to be defined before
|
wants it and none of them owns it, and because it has to be defined before
|
||||||
casino-fx.js — which reaches for it on every chip it throws. `defer` runs
|
casino-fx.js — which reaches for it on every chip it throws. `defer` runs
|
||||||
these in document order, so that's guaranteed. -->
|
these in document order, so that's guaranteed. -->
|
||||||
<script src="/static/js/casino-sfx.js" defer></script>
|
<script src="{{asset "/static/js/casino-sfx.js"}}" defer></script>
|
||||||
{{block "scripts" .}}{{end}}
|
{{block "scripts" .}}{{end}}
|
||||||
</body>
|
</body>
|
||||||
</html>{{end}}
|
</html>{{end}}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div class="flex items-center gap-3 min-w-0">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
<a href="/games" class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-[color:var(--card)] shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition" title="Back to the casino">
|
<a href="/games" class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-card shadow-pete border-2 border-ink/10 hover:bg-ink/5 transition" title="Back to the casino">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5" aria-hidden="true">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5" aria-hidden="true">
|
||||||
<path d="M19 12H5"></path><polyline points="12 19 5 12 12 5"></polyline>
|
<path d="M19 12H5"></path><polyline points="12 19 5 12 12 5"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -13,14 +13,14 @@
|
|||||||
</a>
|
</a>
|
||||||
<h1 class="font-display text-3xl font-bold">Hangman</h1>
|
<h1 class="font-display text-3xl font-bold">Hangman</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-[color:var(--ink)]/50">{{.MaxWrong}} lives · every wrong guess costs you a tenth of the win</p>
|
<p class="text-sm text-muted">{{.MaxWrong}} lives · every wrong guess costs you a tenth of the win</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{template "_chipbar" .}}
|
{{template "_chipbar" .}}
|
||||||
|
|
||||||
<!-- The felt. The gallows is on it because the gallows is the meter: it counts
|
<!-- The felt. The gallows is on it because the gallows is the meter: it counts
|
||||||
down your lives and your winnings at the same time. -->
|
down your lives and your winnings at the same time. -->
|
||||||
<section class="pete-felt relative overflow-hidden rounded-3xl p-6 sm:p-10 shadow-pete-lg border-2 border-[color:var(--ink)]/10">
|
<section class="pete-felt relative overflow-hidden rounded-3xl p-6 sm:p-10 shadow-pete-lg border-2 border-ink/10">
|
||||||
|
|
||||||
<div class="pete-rack" data-house aria-hidden="true">
|
<div class="pete-rack" data-house aria-hidden="true">
|
||||||
<span data-chip="500" style="--stack: 5"></span>
|
<span data-chip="500" style="--stack: 5"></span>
|
||||||
@@ -102,47 +102,47 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Guessing: shown while a game is live. -->
|
<!-- Guessing: shown while a game is live. -->
|
||||||
<section data-guessing class="hidden rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section data-guessing class="hidden rounded-3xl bg-card p-5 sm:p-6 shadow-pete border-2 border-ink/10">
|
||||||
<div data-keyboard class="pete-keys"></div>
|
<div data-keyboard class="pete-keys"></div>
|
||||||
|
|
||||||
<div class="mt-4 flex flex-wrap items-center gap-2">
|
<div class="mt-4 flex flex-wrap items-center gap-2">
|
||||||
<label for="solve" class="sr-only">Guess the whole phrase</label>
|
<label for="solve" class="sr-only">Guess the whole phrase</label>
|
||||||
<input id="solve" data-solve-input type="text" autocomplete="off" enterkeyhint="go"
|
<input id="solve" data-solve-input type="text" autocomplete="off" enterkeyhint="go"
|
||||||
placeholder="Or just say what it is…"
|
placeholder="Or just say what it is…"
|
||||||
class="min-w-0 flex-1 rounded-full bg-[color:var(--ink)]/5 px-4 py-2.5 text-sm font-semibold
|
class="min-w-0 flex-1 rounded-full bg-ink/5 px-4 py-2.5 text-sm font-semibold
|
||||||
border-2 border-[color:var(--ink)]/10 focus:outline-none focus:border-[color:var(--accent)]">
|
border-2 border-ink/10 focus:outline-none focus:border-accent">
|
||||||
<button type="button" data-solve
|
<button type="button" data-solve
|
||||||
class="rounded-full bg-[color:var(--accent)] px-6 py-2.5 font-display font-bold text-white shadow-pete
|
class="rounded-full bg-accent px-6 py-2.5 font-display font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Solve
|
Solve
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 text-center text-xs text-[color:var(--ink)]/40">
|
<p class="mt-3 text-center text-xs text-muted">
|
||||||
Type a letter to guess it. A wrong solve costs a life, same as a wrong letter.
|
Type a letter to guess it. A wrong solve costs a life, same as a wrong letter.
|
||||||
</p>
|
</p>
|
||||||
<p data-game-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
<p data-game-msg class="hidden mt-3 rounded-2xl bg-ink/5 px-4 py-2 text-sm font-semibold"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Betting: shown between games. -->
|
<!-- Betting: shown between games. -->
|
||||||
<section data-betting class="rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section data-betting class="rounded-3xl bg-card p-5 sm:p-6 shadow-pete border-2 border-ink/10">
|
||||||
|
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">How long a phrase?</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">How long a phrase?</div>
|
||||||
<div class="mt-2 grid gap-2 sm:grid-cols-3">
|
<div class="mt-2 grid gap-2 sm:grid-cols-3">
|
||||||
{{range .Tiers}}
|
{{range .Tiers}}
|
||||||
<button type="button" data-tier="{{.Slug}}"
|
<button type="button" data-tier="{{.Slug}}"
|
||||||
class="pete-tier rounded-2xl border-2 p-3 text-left transition">
|
class="pete-tier rounded-2xl border-2 p-3 text-left transition">
|
||||||
<div class="flex items-baseline justify-between gap-2">
|
<div class="flex items-baseline justify-between gap-2">
|
||||||
<span class="font-display text-lg font-bold">{{.Name}}</span>
|
<span class="font-display text-lg font-bold">{{.Name}}</span>
|
||||||
<span class="font-display text-lg font-bold tabular-nums text-[color:var(--accent)]">{{printf "%.1f" .Base}}×</span>
|
<span class="font-display text-lg font-bold tabular-nums text-accent">{{printf "%.1f" .Base}}×</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-0.5 text-xs text-[color:var(--ink)]/50">{{.Blurb}}</p>
|
<p class="mt-0.5 text-xs text-muted">{{.Blurb}}</p>
|
||||||
</button>
|
</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 flex flex-wrap items-center gap-x-6 gap-y-4">
|
<div class="mt-5 flex flex-wrap items-center gap-x-6 gap-y-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">Your bet</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">Your bet</div>
|
||||||
<div class="font-display text-3xl font-bold tabular-nums"><span data-bet-amount>0</span></div>
|
<div class="font-display text-3xl font-bold tabular-nums"><span data-bet-amount>0</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -154,23 +154,23 @@
|
|||||||
</button>
|
</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
<button type="button" data-bet-clear
|
<button type="button" data-bet-clear
|
||||||
class="rounded-full px-3 py-2 text-sm font-semibold text-[color:var(--ink)]/50 hover:text-[color:var(--ink)] transition">Clear</button>
|
class="rounded-full px-3 py-2 text-sm font-semibold text-muted hover:text-ink transition">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" data-start
|
<button type="button" data-start
|
||||||
class="ml-auto rounded-full bg-[color:var(--accent)] px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
class="ml-auto rounded-full bg-accent px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Play
|
Play
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p data-table-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
<p data-table-msg class="hidden mt-3 rounded-2xl bg-ink/5 px-4 py-2 text-sm font-semibold"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "scripts"}}
|
{{define "scripts"}}
|
||||||
<script src="/static/js/casino-fx.js" defer></script>
|
<script src="{{asset "/static/js/casino-fx.js"}}" defer></script>
|
||||||
<script src="/static/js/games.js" defer></script>
|
<script src="{{asset "/static/js/games.js"}}" defer></script>
|
||||||
<script src="/static/js/hangman.js" defer></script>
|
<script src="{{asset "/static/js/hangman.js"}}" defer></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div class="flex items-center gap-3 min-w-0">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
<a href="/games" class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-[color:var(--card)] shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition" title="Back to the casino">
|
<a href="/games" class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-card shadow-pete border-2 border-ink/10 hover:bg-ink/5 transition" title="Back to the casino">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5" aria-hidden="true">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5" aria-hidden="true">
|
||||||
<path d="M19 12H5"></path><polyline points="12 19 5 12 12 5"></polyline>
|
<path d="M19 12H5"></path><polyline points="12 19 5 12 12 5"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<h1 class="font-display text-3xl font-bold">Texas Hold'em</h1>
|
<h1 class="font-display text-3xl font-bold">Texas Hold'em</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-[color:var(--ink)]/50">Buy in, play as long as you like, leave with what's in front of you</p>
|
<p class="text-sm text-muted">Buy in, play as long as you like, leave with what's in front of you</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{template "_chipbar" .}}
|
{{template "_chipbar" .}}
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
travelling between one of those and the pot — which is the whole difference
|
travelling between one of those and the pot — which is the whole difference
|
||||||
between poker and every other game in the room, where the chips only ever
|
between poker and every other game in the room, where the chips only ever
|
||||||
move between you and the house. -->
|
move between you and the house. -->
|
||||||
<section class="pete-felt pete-poker relative overflow-hidden rounded-3xl p-4 sm:p-6 lg:p-8 shadow-pete-lg border-2 border-[color:var(--ink)]/10">
|
<section class="pete-felt pete-poker relative overflow-hidden rounded-3xl p-4 sm:p-6 lg:p-8 shadow-pete-lg border-2 border-ink/10">
|
||||||
<div class="grid gap-5 lg:grid-cols-[minmax(0,1fr),auto] lg:gap-8">
|
<div class="grid gap-5 lg:grid-cols-[minmax(0,1fr),auto] lg:gap-8">
|
||||||
|
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
@@ -75,95 +75,95 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Acting: shown when the hand is yours to play. -->
|
<!-- Acting: shown when the hand is yours to play. -->
|
||||||
<section data-acting class="hidden rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section data-acting class="hidden rounded-3xl bg-card p-5 sm:p-6 shadow-pete border-2 border-ink/10">
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<button type="button" data-move="fold"
|
<button type="button" data-move="fold"
|
||||||
class="rounded-full border-2 border-[color:var(--ink)]/15 bg-[color:var(--card)] px-6 py-3 font-display text-lg font-bold shadow-pete
|
class="rounded-full border-2 border-ink/15 bg-card px-6 py-3 font-display text-lg font-bold shadow-pete
|
||||||
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:bg-ink/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Fold
|
Fold
|
||||||
</button>
|
</button>
|
||||||
<button type="button" data-move="check"
|
<button type="button" data-move="check"
|
||||||
class="rounded-full border-2 border-[color:var(--ink)]/15 bg-[color:var(--card)] px-6 py-3 font-display text-lg font-bold shadow-pete
|
class="rounded-full border-2 border-ink/15 bg-card px-6 py-3 font-display text-lg font-bold shadow-pete
|
||||||
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:bg-ink/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Check
|
Check
|
||||||
</button>
|
</button>
|
||||||
<button type="button" data-move="call"
|
<button type="button" data-move="call"
|
||||||
class="rounded-full bg-[color:var(--accent)] px-6 py-3 font-display text-lg font-bold text-white shadow-pete
|
class="rounded-full bg-accent px-6 py-3 font-display text-lg font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Call <span data-call-amount class="tabular-nums"></span>
|
Call <span data-call-amount class="tabular-nums"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-raise-row class="mt-4 flex flex-wrap items-center gap-3 border-t-2 border-[color:var(--ink)]/5 pt-4">
|
<div data-raise-row class="mt-4 flex flex-wrap items-center gap-3 border-t-2 border-ink/5 pt-4">
|
||||||
<div class="pete-raise">
|
<div class="pete-raise">
|
||||||
<input type="range" data-raise-slider aria-label="How much to raise to">
|
<input type="range" data-raise-slider aria-label="How much to raise to">
|
||||||
<span data-raise-to class="pete-raise-to">0</span>
|
<span data-raise-to class="pete-raise-to">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-1.5">
|
<div class="flex flex-wrap items-center gap-1.5">
|
||||||
<button type="button" data-raise-preset="0.5" class="rounded-full border-2 border-[color:var(--ink)]/10 px-3 py-1.5 text-xs font-bold hover:bg-[color:var(--ink)]/5 transition">½ pot</button>
|
<button type="button" data-raise-preset="0.5" class="rounded-full border-2 border-ink/10 px-3 py-1.5 text-xs font-bold hover:bg-ink/5 transition">½ pot</button>
|
||||||
<button type="button" data-raise-preset="1" class="rounded-full border-2 border-[color:var(--ink)]/10 px-3 py-1.5 text-xs font-bold hover:bg-[color:var(--ink)]/5 transition">Pot</button>
|
<button type="button" data-raise-preset="1" class="rounded-full border-2 border-ink/10 px-3 py-1.5 text-xs font-bold hover:bg-ink/5 transition">Pot</button>
|
||||||
<button type="button" data-raise-preset="max" class="rounded-full border-2 border-[color:var(--ink)]/10 px-3 py-1.5 text-xs font-bold hover:bg-[color:var(--ink)]/5 transition">Max</button>
|
<button type="button" data-raise-preset="max" class="rounded-full border-2 border-ink/10 px-3 py-1.5 text-xs font-bold hover:bg-ink/5 transition">Max</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" data-move="raise"
|
<button type="button" data-move="raise"
|
||||||
class="ml-auto rounded-full bg-[color:var(--accent)] px-7 py-3 font-display text-lg font-bold text-white shadow-pete
|
class="ml-auto rounded-full bg-accent px-7 py-3 font-display text-lg font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
<span data-raise-verb>Raise to</span> <span data-raise-label class="tabular-nums"></span>
|
<span data-raise-verb>Raise to</span> <span data-raise-label class="tabular-nums"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p data-game-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
<p data-game-msg class="hidden mt-3 rounded-2xl bg-ink/5 px-4 py-2 text-sm font-semibold"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Between hands: deal the next one, put more chips out, or get up. -->
|
<!-- Between hands: deal the next one, put more chips out, or get up. -->
|
||||||
<section data-between class="hidden rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section data-between class="hidden rounded-3xl bg-card p-5 sm:p-6 shadow-pete border-2 border-ink/10">
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">In front of you</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">In front of you</div>
|
||||||
<div class="font-display text-3xl font-bold tabular-nums" data-table-stack>0</div>
|
<div class="font-display text-3xl font-bold tabular-nums" data-table-stack>0</div>
|
||||||
<p class="mt-0.5 text-xs text-[color:var(--ink)]/40">
|
<p class="mt-0.5 text-xs text-muted">
|
||||||
Bought in for <span data-bought-in class="tabular-nums">0</span> · the house has taken <span data-session-rake class="tabular-nums">0</span> in rake
|
Bought in for <span data-bought-in class="tabular-nums">0</span> · the house has taken <span data-session-rake class="tabular-nums">0</span> in rake
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-auto flex flex-wrap items-center gap-2">
|
<div class="ml-auto flex flex-wrap items-center gap-2">
|
||||||
<button type="button" data-topup
|
<button type="button" data-topup
|
||||||
class="rounded-full border-2 border-[color:var(--ink)]/15 bg-[color:var(--card)] px-5 py-3 font-display text-base font-bold shadow-pete
|
class="rounded-full border-2 border-ink/15 bg-card px-5 py-3 font-display text-base font-bold shadow-pete
|
||||||
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:bg-ink/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Top up
|
Top up
|
||||||
</button>
|
</button>
|
||||||
<button type="button" data-leave
|
<button type="button" data-leave
|
||||||
class="rounded-full border-2 border-[color:var(--ink)]/15 bg-[color:var(--card)] px-5 py-3 font-display text-base font-bold shadow-pete
|
class="rounded-full border-2 border-ink/15 bg-card px-5 py-3 font-display text-base font-bold shadow-pete
|
||||||
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:bg-ink/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Get up
|
Get up
|
||||||
</button>
|
</button>
|
||||||
<button type="button" data-deal
|
<button type="button" data-deal
|
||||||
class="rounded-full bg-[color:var(--accent)] px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
class="rounded-full bg-accent px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Next hand
|
Next hand
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p data-between-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
<p data-between-msg class="hidden mt-3 rounded-2xl bg-ink/5 px-4 py-2 text-sm font-semibold"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- The rail talk. Chat lives at the felt and does not leave it for Matrix;
|
<!-- The rail talk. Chat lives at the felt and does not leave it for Matrix;
|
||||||
every line is stamped with the hand it was said during. Shown while you're
|
every line is stamped with the hand it was said during. Shown while you're
|
||||||
seated. -->
|
seated. -->
|
||||||
<section data-chat class="hidden rounded-3xl bg-[color:var(--card)] p-4 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section data-chat class="hidden rounded-3xl bg-card p-4 shadow-pete border-2 border-ink/10">
|
||||||
<div data-chat-log class="flex flex-col gap-1 max-h-40 overflow-y-auto pr-1 text-sm" aria-live="polite" aria-label="Table chat"></div>
|
<div data-chat-log class="flex flex-col gap-1 max-h-40 overflow-y-auto pr-1 text-sm" aria-live="polite" aria-label="Table chat"></div>
|
||||||
<form data-chat-form class="mt-3 flex items-center gap-2">
|
<form data-chat-form class="mt-3 flex items-center gap-2">
|
||||||
<input data-chat-input type="text" maxlength="240" autocomplete="off" placeholder="Say something to the table…"
|
<input data-chat-input type="text" maxlength="240" autocomplete="off" placeholder="Say something to the table…"
|
||||||
class="flex-1 min-w-0 rounded-full border-2 border-[color:var(--ink)]/15 bg-[color:var(--bg)] px-4 py-2 text-sm focus:outline-none focus:border-[color:var(--accent)]">
|
class="flex-1 min-w-0 rounded-full border-2 border-ink/15 bg-[color:var(--bg)] px-4 py-2 text-sm focus:outline-none focus:border-accent">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="rounded-full bg-[color:var(--accent)] px-5 py-2 font-display text-sm font-bold text-white shadow-pete hover:brightness-105 active:translate-y-px transition">
|
class="rounded-full bg-accent px-5 py-2 font-display text-sm font-bold text-white shadow-pete hover:brightness-105 active:translate-y-px transition">
|
||||||
Say
|
Say
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Sitting down: shown when you aren't at a table. -->
|
<!-- Sitting down: shown when you aren't at a table. -->
|
||||||
<section data-sitting class="rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section data-sitting class="rounded-3xl bg-card p-5 sm:p-6 shadow-pete border-2 border-ink/10">
|
||||||
|
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">What are you playing for?</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">What are you playing for?</div>
|
||||||
<div class="mt-2 grid gap-2 sm:grid-cols-3">
|
<div class="mt-2 grid gap-2 sm:grid-cols-3">
|
||||||
{{range .Stakes}}
|
{{range .Stakes}}
|
||||||
<button type="button" data-tier="{{.Slug}}"
|
<button type="button" data-tier="{{.Slug}}"
|
||||||
@@ -171,10 +171,10 @@
|
|||||||
class="pete-tier rounded-2xl border-2 p-3 text-left transition">
|
class="pete-tier rounded-2xl border-2 p-3 text-left transition">
|
||||||
<div class="flex items-baseline justify-between gap-2">
|
<div class="flex items-baseline justify-between gap-2">
|
||||||
<span class="font-display text-lg font-bold">{{.Name}}</span>
|
<span class="font-display text-lg font-bold">{{.Name}}</span>
|
||||||
<span class="font-display text-lg font-bold tabular-nums text-[color:var(--accent)]">{{.SB}}/{{.BB}}</span>
|
<span class="font-display text-lg font-bold tabular-nums text-accent">{{.SB}}/{{.BB}}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-0.5 text-xs text-[color:var(--ink)]/50">{{.Blurb}}</p>
|
<p class="mt-0.5 text-xs text-muted">{{.Blurb}}</p>
|
||||||
<p class="mt-1.5 text-xs font-semibold text-[color:var(--ink)]/40">
|
<p class="mt-1.5 text-xs font-semibold text-muted">
|
||||||
Buy in {{.MinBuy}}–{{.MaxBuy}}
|
Buy in {{.MinBuy}}–{{.MaxBuy}}
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
|
|
||||||
<div class="mt-5 grid gap-5 sm:grid-cols-2">
|
<div class="mt-5 grid gap-5 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">How many of them?</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">How many of them?</div>
|
||||||
<div class="mt-2 flex flex-wrap gap-1.5" data-bots>
|
<div class="mt-2 flex flex-wrap gap-1.5" data-bots>
|
||||||
<button type="button" data-bot-count="1" class="pete-tier rounded-xl border-2 px-4 py-2 text-sm font-bold transition">1</button>
|
<button type="button" data-bot-count="1" class="pete-tier rounded-xl border-2 px-4 py-2 text-sm font-bold transition">1</button>
|
||||||
<button type="button" data-bot-count="2" class="pete-tier rounded-xl border-2 px-4 py-2 text-sm font-bold transition">2</button>
|
<button type="button" data-bot-count="2" class="pete-tier rounded-xl border-2 px-4 py-2 text-sm font-bold transition">2</button>
|
||||||
@@ -191,21 +191,21 @@
|
|||||||
<button type="button" data-bot-count="4" class="pete-tier rounded-xl border-2 px-4 py-2 text-sm font-bold transition">4</button>
|
<button type="button" data-bot-count="4" class="pete-tier rounded-xl border-2 px-4 py-2 text-sm font-bold transition">4</button>
|
||||||
<button type="button" data-bot-count="5" class="pete-tier rounded-xl border-2 px-4 py-2 text-sm font-bold transition">5</button>
|
<button type="button" data-bot-count="5" class="pete-tier rounded-xl border-2 px-4 py-2 text-sm font-bold transition">5</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1.5 text-xs text-[color:var(--ink)]/40" data-bots-note></p>
|
<p class="mt-1.5 text-xs text-muted" data-bots-note></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">Buying in for</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">Buying in for</div>
|
||||||
<div class="mt-2 flex items-center gap-3">
|
<div class="mt-2 flex items-center gap-3">
|
||||||
<input type="range" data-buyin-slider class="flex-1 min-w-0" aria-label="How much to buy in for">
|
<input type="range" data-buyin-slider class="flex-1 min-w-0" aria-label="How much to buy in for">
|
||||||
<span data-buyin class="font-display text-2xl font-bold tabular-nums">0</span>
|
<span data-buyin class="font-display text-2xl font-bold tabular-nums">0</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1.5 text-xs text-[color:var(--ink)]/40" data-buyin-note></p>
|
<p class="mt-1.5 text-xs text-muted" data-buyin-note></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" data-sit
|
<button type="button" data-sit
|
||||||
class="mt-5 w-full rounded-full bg-[color:var(--accent)] px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
class="mt-5 w-full rounded-full bg-accent px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Sit down
|
Sit down
|
||||||
</button>
|
</button>
|
||||||
@@ -213,22 +213,22 @@
|
|||||||
<!-- Or pull up a chair at a table someone else has already opened. Bots keep
|
<!-- Or pull up a chair at a table someone else has already opened. Bots keep
|
||||||
every open table full, so there is always a seat to take. -->
|
every open table full, so there is always a seat to take. -->
|
||||||
<div data-lobby-wrap class="mt-6 hidden">
|
<div data-lobby-wrap class="mt-6 hidden">
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">Or join a table in progress</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">Or join a table in progress</div>
|
||||||
<div data-lobby class="mt-2 grid gap-2"></div>
|
<div data-lobby class="mt-2 grid gap-2"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mt-3 text-center text-xs text-[color:var(--ink)]/40">
|
<p class="mt-3 text-center text-xs text-muted">
|
||||||
The bots are trained, not scripted. The house takes {{.RakePct}}% of a pot that sees a flop, capped at three big blinds, and nothing at all from a hand that doesn't.
|
The bots are trained, not scripted. The house takes {{.RakePct}}% of a pot that sees a flop, capped at three big blinds, and nothing at all from a hand that doesn't.
|
||||||
</p>
|
</p>
|
||||||
<p data-table-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
<p data-table-msg class="hidden mt-3 rounded-2xl bg-ink/5 px-4 py-2 text-sm font-semibold"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "scripts"}}
|
{{define "scripts"}}
|
||||||
<script src="/static/js/casino-fx.js" defer></script>
|
<script src="{{asset "/static/js/casino-fx.js"}}" defer></script>
|
||||||
<script src="/static/js/casino-cards.js" defer></script>
|
<script src="{{asset "/static/js/casino-cards.js"}}" defer></script>
|
||||||
<script src="/static/js/games.js" defer></script>
|
<script src="{{asset "/static/js/games.js"}}" defer></script>
|
||||||
<script src="/static/js/holdem.js" defer></script>
|
<script src="{{asset "/static/js/holdem.js"}}" defer></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<h1 class="font-display text-4xl sm:text-6xl font-bold leading-tight">
|
<h1 class="font-display text-4xl sm:text-6xl font-bold leading-tight">
|
||||||
a friendlier news feed.
|
a friendlier news feed.
|
||||||
</h1>
|
</h1>
|
||||||
<p class="mx-auto mt-3 max-w-xl text-base sm:text-lg text-[color:var(--ink)]/70">
|
<p class="mx-auto mt-3 max-w-xl text-base sm:text-lg text-muted">
|
||||||
{{if .PostingEnabled}}
|
{{if .PostingEnabled}}
|
||||||
Pete reads RSS, sorts stories, and posts them to Matrix.
|
Pete reads RSS, sorts stories, and posts them to Matrix.
|
||||||
Glowing cards are the ones he's posted.
|
Glowing cards are the ones he's posted.
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
Pete reads RSS and sorts stories into channels, fresh from his feeds.
|
Pete reads RSS and sorts stories into channels, fresh from his feeds.
|
||||||
{{end}}
|
{{end}}
|
||||||
</p>
|
</p>
|
||||||
<p class="mx-auto mt-5 inline-flex items-center gap-2 rounded-full bg-[color:var(--card)] px-4 py-2 text-sm font-semibold shadow-pete border-2 border-[color:var(--ink)]/10">
|
<p class="mx-auto mt-5 inline-flex items-center gap-2 rounded-full bg-card px-4 py-2 text-sm font-semibold shadow-pete border-2 border-ink/10">
|
||||||
<img src="/static/img/pete.avif" alt="Pete" width="24" height="24"
|
<img src="{{asset "/static/img/pete.avif"}}" alt="Pete" width="24" height="24"
|
||||||
class="h-6 w-6 rounded-full object-cover">
|
class="h-6 w-6 rounded-full object-cover">
|
||||||
<span data-pete-greeting>Hey, I'm Pete. Here's what's new.</span>
|
<span data-pete-greeting>Hey, I'm Pete. Here's what's new.</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<h2 class="font-display text-xl sm:text-2xl font-bold">
|
<h2 class="font-display text-xl sm:text-2xl font-bold">
|
||||||
<span aria-hidden="true">🔥</span> Popular this week
|
<span aria-hidden="true">🔥</span> Popular this week
|
||||||
</h2>
|
</h2>
|
||||||
<span class="text-xs text-[color:var(--ink)]/50">what readers are opening most</span>
|
<span class="text-xs text-muted">what readers are opening most</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{{range .Trending}}
|
{{range .Trending}}
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<h2 class="font-display text-xl sm:text-2xl font-bold">
|
<h2 class="font-display text-xl sm:text-2xl font-bold">
|
||||||
<span aria-hidden="true">✨</span> For you
|
<span aria-hidden="true">✨</span> For you
|
||||||
</h2>
|
</h2>
|
||||||
<a href="/for-you" class="text-xs font-semibold text-[color:var(--ink)]/60 hover:text-[color:var(--ink)]">more →</a>
|
<a href="/for-you" class="text-xs font-semibold text-muted hover:text-ink">more →</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{{range .ForYou}}
|
{{range .ForYou}}
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
<h2 class="font-display text-xl sm:text-2xl font-bold">
|
<h2 class="font-display text-xl sm:text-2xl font-bold">
|
||||||
<span aria-hidden="true">📡</span> Pete just posted
|
<span aria-hidden="true">📡</span> Pete just posted
|
||||||
</h2>
|
</h2>
|
||||||
<span class="text-xs text-[color:var(--ink)]/50">most recent posts to Matrix</span>
|
<span class="text-xs text-muted">most recent posts to Matrix</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{{range .JustPosted}}
|
{{range .JustPosted}}
|
||||||
@@ -75,34 +75,34 @@
|
|||||||
<h2 class="font-display text-xl sm:text-2xl font-bold">
|
<h2 class="font-display text-xl sm:text-2xl font-bold">
|
||||||
<span aria-hidden="true">📊</span> Channels
|
<span aria-hidden="true">📊</span> Channels
|
||||||
</h2>
|
</h2>
|
||||||
<span class="text-xs text-[color:var(--ink)]/50">{{if .PostingEnabled}}last 24 hours{{else}}what Pete's tracking{{end}}</span>
|
<span class="text-xs text-muted">{{if .PostingEnabled}}last 24 hours{{else}}what Pete's tracking{{end}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
||||||
{{range .Stats}}
|
{{range .Stats}}
|
||||||
<a href="/{{.Channel.Slug}}"
|
<a href="/{{.Channel.Slug}}"
|
||||||
class="block rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 shadow-pete p-4 hover:-translate-y-1 hover:shadow-pete-lg transition">
|
class="block rounded-3xl bg-card border-2 border-ink/10 shadow-pete p-4 hover:-translate-y-1 hover:shadow-pete-lg transition">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="inline-flex items-center justify-center h-9 w-9 rounded-2xl bg-theme-{{.Channel.Theme}} text-white text-lg">
|
<span class="inline-flex items-center justify-center h-9 w-9 rounded-2xl bg-theme-{{.Channel.Theme}} text-white text-lg">
|
||||||
{{.Channel.Emoji}}
|
{{.Channel.Emoji}}
|
||||||
</span>
|
</span>
|
||||||
<span class="font-display text-lg font-semibold">{{.Channel.Title}}</span>
|
<span class="font-display text-lg font-semibold">{{.Channel.Title}}</span>
|
||||||
</div>
|
</div>
|
||||||
<dl class="mt-3 space-y-1 text-xs text-[color:var(--ink)]/70">
|
<dl class="mt-3 space-y-1 text-xs text-muted">
|
||||||
{{if $.PostingEnabled}}
|
{{if $.PostingEnabled}}
|
||||||
<div class="flex justify-between gap-2">
|
<div class="flex justify-between gap-2">
|
||||||
<dt>last post</dt>
|
<dt>last post</dt>
|
||||||
<dd class="font-semibold text-[color:var(--ink)]">
|
<dd class="font-semibold text-ink">
|
||||||
{{if .HasLastPosted}}{{timeAgo .LastPostedAt}}{{else}}—{{end}}
|
{{if .HasLastPosted}}{{timeAgo .LastPostedAt}}{{else}}—{{end}}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between gap-2">
|
<div class="flex justify-between gap-2">
|
||||||
<dt>posted 24h</dt>
|
<dt>posted 24h</dt>
|
||||||
<dd class="font-semibold text-[color:var(--ink)]">{{.PostsToday}}</dd>
|
<dd class="font-semibold text-ink">{{.PostsToday}}</dd>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="flex justify-between gap-2">
|
<div class="flex justify-between gap-2">
|
||||||
<dt>stories</dt>
|
<dt>stories</dt>
|
||||||
<dd class="font-semibold text-[color:var(--ink)]">{{.Total}}</dd>
|
<dd class="font-semibold text-ink">{{.Total}}</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</a>
|
</a>
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
<h2 class="font-display text-xl sm:text-2xl font-bold">
|
<h2 class="font-display text-xl sm:text-2xl font-bold">
|
||||||
<span aria-hidden="true">🗞️</span> Latest across the feed
|
<span aria-hidden="true">🗞️</span> Latest across the feed
|
||||||
</h2>
|
</h2>
|
||||||
<span class="text-xs text-[color:var(--ink)]/50">newest first, all channels</span>
|
<span class="text-xs text-muted">newest first, all channels</span>
|
||||||
</div>
|
</div>
|
||||||
{{if .Latest}}
|
{{if .Latest}}
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-dashed border-[color:var(--ink)]/15 p-8 text-center text-[color:var(--ink)]/60">
|
<div class="rounded-3xl bg-card border-2 border-dashed border-ink/15 p-8 text-center text-muted">
|
||||||
Nothing here yet. Pete's still listening for the first story.
|
Nothing here yet. Pete's still listening for the first story.
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -9,14 +9,14 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/css/output.css">
|
<link rel="stylesheet" href="{{asset "/static/css/output.css"}}">
|
||||||
<link rel="icon" href="/static/img/pete.avif" type="image/avif">
|
<link rel="icon" href="{{asset "/static/img/pete.avif"}}" type="image/avif">
|
||||||
<link rel="manifest" href="/manifest.webmanifest">
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
<meta name="theme-color" content="#fbf3e3">
|
<meta name="theme-color" content="#fbf3e3">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
<meta name="apple-mobile-web-app-title" content="Pete">
|
<meta name="apple-mobile-web-app-title" content="Pete">
|
||||||
<link rel="apple-touch-icon" href="/static/img/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="{{asset "/static/img/apple-touch-icon.png"}}">
|
||||||
<link rel="alternate" type="application/rss+xml" title="{{.SiteTitle}} — all stories" href="/feed.xml">
|
<link rel="alternate" type="application/rss+xml" title="{{.SiteTitle}} — all stories" href="/feed.xml">
|
||||||
<link rel="alternate" type="application/feed+json" title="{{.SiteTitle}} — all stories" href="/feed.json">
|
<link rel="alternate" type="application/feed+json" title="{{.SiteTitle}} — all stories" href="/feed.json">
|
||||||
{{if .Active}}{{if ne .Active "bookmarks"}}
|
{{if .Active}}{{if ne .Active "bookmarks"}}
|
||||||
@@ -40,32 +40,32 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen bg-[color:var(--bg)] text-[color:var(--ink)] transition-colors duration-1000">
|
<body class="min-h-screen bg-[color:var(--bg)] text-ink transition-colors duration-1000">
|
||||||
<div class="pointer-events-none fixed inset-0 -z-10 bg-[color:var(--bg-grad)] transition-colors duration-1000"></div>
|
<div class="pointer-events-none fixed inset-0 -z-10 bg-[color:var(--bg-grad)] transition-colors duration-1000"></div>
|
||||||
<canvas id="pete-weather" class="pointer-events-none fixed inset-0 -z-[5] h-full w-full" aria-hidden="true"></canvas>
|
<canvas id="pete-weather" class="pointer-events-none fixed inset-0 -z-[5] h-full w-full" aria-hidden="true"></canvas>
|
||||||
|
|
||||||
<header class="mx-auto max-w-6xl px-4 pt-8 pb-4 sm:pt-12">
|
<header class="mx-auto max-w-6xl px-4 pt-8 pb-4 sm:pt-12">
|
||||||
<div class="flex items-center justify-between gap-3 sm:gap-4">
|
<div class="flex items-center justify-between gap-3 sm:gap-4">
|
||||||
<a href="/" class="flex items-center gap-3 group min-w-0">
|
<a href="/" class="flex items-center gap-3 group min-w-0">
|
||||||
<img src="/static/img/pete.avif" alt="Pete" width="48" height="48"
|
<img src="{{asset "/static/img/pete.avif"}}" alt="Pete" width="48" height="48"
|
||||||
class="h-12 w-12 shrink-0 rounded-2xl bg-[color:var(--card)] shadow-pete border-2 border-[color:var(--ink)]/10 object-cover group-hover:-rotate-6 transition-transform">
|
class="h-12 w-12 shrink-0 rounded-2xl bg-card shadow-pete border-2 border-ink/10 object-cover group-hover:-rotate-6 transition-transform">
|
||||||
<span class="font-display text-3xl font-bold tracking-tight">{{.SiteTitle}}</span>
|
<span class="font-display text-3xl font-bold tracking-tight">{{.SiteTitle}}</span>
|
||||||
<span class="hidden sm:inline rounded-full bg-[color:var(--accent)]/20 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/70" data-phase-label>day</span>
|
<span class="hidden sm:inline rounded-full bg-accent/20 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-muted" data-phase-label>day</span>
|
||||||
</a>
|
</a>
|
||||||
{{if .AuthEnabled}}
|
{{if .AuthEnabled}}
|
||||||
{{if .User}}
|
{{if .User}}
|
||||||
<a href="/auth/logout" data-account
|
<a href="/auth/logout" data-account
|
||||||
title="Signed in as {{.User.Display}}{{if .User.Email}} · {{.User.Email}}{{end}} — sign out"
|
title="Signed in as {{.User.Display}}{{if .User.Email}} · {{.User.Email}}{{end}} — sign out"
|
||||||
class="inline-flex shrink-0 items-center gap-2 rounded-full bg-[color:var(--card)] px-2.5 py-1.5 text-sm font-semibold shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
|
class="inline-flex shrink-0 items-center gap-2 rounded-full bg-card px-2.5 py-1.5 text-sm font-semibold shadow-pete border-2 border-ink/10 hover:bg-ink/5 transition">
|
||||||
<span class="grid h-6 w-6 place-items-center rounded-full bg-[color:var(--accent)] text-white text-xs font-bold">{{.User.Initial}}</span>
|
<span class="grid h-6 w-6 place-items-center rounded-full bg-accent text-white text-xs font-bold">{{.User.Initial}}</span>
|
||||||
<span class="hidden sm:inline max-w-[7rem] truncate">{{.User.Display}}</span>
|
<span class="hidden sm:inline max-w-[7rem] truncate">{{.User.Display}}</span>
|
||||||
</a>
|
</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<a href="/auth/login?next={{.Path}}" data-signin
|
<a href="/auth/login?next={{.Path}}" data-signin
|
||||||
title="Sign in to sync your preferences"
|
title="Sign in to sync your preferences"
|
||||||
class="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-[color:var(--card)] px-3 py-2 text-sm font-semibold shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
|
class="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-card px-3 py-2 text-sm font-semibold shadow-pete border-2 border-ink/10 hover:bg-ink/5 transition">
|
||||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4 text-[color:var(--ink)]/60">
|
stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4 text-muted">
|
||||||
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path>
|
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path>
|
||||||
<polyline points="10 17 15 12 10 7"></polyline>
|
<polyline points="10 17 15 12 10 7"></polyline>
|
||||||
<line x1="15" y1="12" x2="3" y2="12"></line>
|
<line x1="15" y1="12" x2="3" y2="12"></line>
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
<div class="mt-3 flex flex-wrap items-center gap-2 sm:gap-3">
|
<div class="mt-3 flex flex-wrap items-center gap-2 sm:gap-3">
|
||||||
<button type="button" data-settings-trigger
|
<button type="button" data-settings-trigger
|
||||||
title="Feed settings"
|
title="Feed settings"
|
||||||
class="inline-flex shrink-0 items-center justify-center rounded-full bg-[color:var(--card)] p-2 shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
|
class="inline-flex shrink-0 items-center justify-center rounded-full bg-card p-2 shadow-pete border-2 border-ink/10 hover:bg-ink/5 transition">
|
||||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
|
stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
|
||||||
<circle cx="12" cy="12" r="3"></circle>
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button type="button" data-reader-open
|
<button type="button" data-reader-open
|
||||||
title="Feed mode — read one story at a time (press f)"
|
title="Feed mode — read one story at a time (press f)"
|
||||||
class="inline-flex shrink-0 items-center justify-center rounded-full bg-[color:var(--card)] p-2 shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
|
class="inline-flex shrink-0 items-center justify-center rounded-full bg-card p-2 shadow-pete border-2 border-ink/10 hover:bg-ink/5 transition">
|
||||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
|
stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
|
||||||
<path d="M2 4h6a3 3 0 0 1 3 3v13a2.5 2.5 0 0 0-2.5-2.5H2z"></path>
|
<path d="M2 4h6a3 3 0 0 1 3 3v13a2.5 2.5 0 0 0-2.5-2.5H2z"></path>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
{{if .Weather.Variant}}
|
{{if .Weather.Variant}}
|
||||||
<button type="button" data-weather-toggle aria-pressed="true"
|
<button type="button" data-weather-toggle aria-pressed="true"
|
||||||
title="Toggle weather animation"
|
title="Toggle weather animation"
|
||||||
class="inline-flex shrink-0 items-center justify-center rounded-full bg-[color:var(--card)] p-2 shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
|
class="inline-flex shrink-0 items-center justify-center rounded-full bg-card p-2 shadow-pete border-2 border-ink/10 hover:bg-ink/5 transition">
|
||||||
<svg aria-hidden="true" viewBox="0 0 24 24" class="h-5 w-5 transition-colors"
|
<svg aria-hidden="true" viewBox="0 0 24 24" class="h-5 w-5 transition-colors"
|
||||||
data-weather-star fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round">
|
data-weather-star fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round">
|
||||||
<polygon points="12,2.7 14.85,9.27 21.95,10.05 16.6,14.83 18.18,21.8 12,18.13 5.82,21.8 7.4,14.83 2.05,10.05 9.15,9.27"/>
|
<polygon points="12,2.7 14.85,9.27 21.95,10.05 16.6,14.83 18.18,21.8 12,18.13 5.82,21.8 7.4,14.83 2.05,10.05 9.15,9.27"/>
|
||||||
@@ -109,14 +109,14 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
<button type="button" data-weather-loc
|
<button type="button" data-weather-loc
|
||||||
title="Set your location"
|
title="Set your location"
|
||||||
class="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-[color:var(--card)] px-3 py-2 text-sm font-semibold shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
|
class="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-card px-3 py-2 text-sm font-semibold shadow-pete border-2 border-ink/10 hover:bg-ink/5 transition">
|
||||||
<span data-weather-chip class="tabular-nums">📍 Weather</span>
|
<span data-weather-chip class="tabular-nums">📍 Weather</span>
|
||||||
</button>
|
</button>
|
||||||
<span data-aqi-chip
|
<span data-aqi-chip
|
||||||
class="hidden shrink-0 items-center gap-1.5 rounded-full bg-[color:var(--card)] px-3 py-2 text-sm font-semibold shadow-pete border-2 border-[color:var(--ink)]/10 tabular-nums"></span>
|
class="hidden shrink-0 items-center gap-1.5 rounded-full bg-card px-3 py-2 text-sm font-semibold shadow-pete border-2 border-ink/10 tabular-nums"></span>
|
||||||
<a href="/status" data-status-link
|
<a href="/status" data-status-link
|
||||||
title="Source health"
|
title="Source health"
|
||||||
class="inline-flex shrink-0 items-center justify-center rounded-full bg-[color:var(--card)] p-2 shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition{{if eq .Active "status"}} bg-[color:var(--accent)]/20{{end}}">
|
class="inline-flex shrink-0 items-center justify-center rounded-full bg-card p-2 shadow-pete border-2 border-ink/10 hover:bg-ink/5 transition{{if eq .Active "status"}} bg-accent/20{{end}}">
|
||||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
|
stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
|
||||||
<path d="M3 12h4l2 6 4-14 2 8h6"></path>
|
<path d="M3 12h4l2 6 4-14 2 8h6"></path>
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
{{if .User}}
|
{{if .User}}
|
||||||
<a href="/bookmarks" data-bookmarks-link
|
<a href="/bookmarks" data-bookmarks-link
|
||||||
title="Your bookmarks"
|
title="Your bookmarks"
|
||||||
class="inline-flex shrink-0 items-center justify-center rounded-full bg-[color:var(--card)] p-2 shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition{{if eq .Active "bookmarks"}} bg-[color:var(--accent)]/20{{end}}">
|
class="inline-flex shrink-0 items-center justify-center rounded-full bg-card p-2 shadow-pete border-2 border-ink/10 hover:bg-ink/5 transition{{if eq .Active "bookmarks"}} bg-accent/20{{end}}">
|
||||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
|
stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5">
|
||||||
<path d="M6 2h12a1 1 0 0 1 1 1v18l-7-4-7 4V3a1 1 0 0 1 1-1z"></path>
|
<path d="M6 2h12a1 1 0 0 1 1 1v18l-7-4-7 4V3a1 1 0 0 1 1-1z"></path>
|
||||||
@@ -136,29 +136,29 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
<div class="flex basis-full items-center gap-2 min-w-0 max-w-full sm:basis-auto sm:ml-auto">
|
<div class="flex basis-full items-center gap-2 min-w-0 max-w-full sm:basis-auto sm:ml-auto">
|
||||||
<button type="button" data-search-trigger title="Search (⌘K)"
|
<button type="button" data-search-trigger title="Search (⌘K)"
|
||||||
class="flex shrink-0 items-center gap-2 rounded-full bg-[color:var(--card)] px-3 py-2 text-sm shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition">
|
class="flex shrink-0 items-center gap-2 rounded-full bg-card px-3 py-2 text-sm shadow-pete border-2 border-ink/10 hover:bg-ink/5 transition">
|
||||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4 text-[color:var(--ink)]/60">
|
stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4 text-muted">
|
||||||
<circle cx="11" cy="11" r="7"></circle>
|
<circle cx="11" cy="11" r="7"></circle>
|
||||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="hidden sm:inline text-[color:var(--ink)]/60 font-semibold">Search</span>
|
<span class="hidden sm:inline text-muted font-semibold">Search</span>
|
||||||
<kbd class="hidden sm:inline-flex items-center rounded-md bg-[color:var(--ink)]/5 px-1.5 py-0.5 text-[10px] font-mono font-semibold text-[color:var(--ink)]/60">⌘K</kbd>
|
<kbd class="hidden sm:inline-flex items-center rounded-md bg-ink/5 px-1.5 py-0.5 text-[10px] font-mono font-semibold text-muted">⌘K</kbd>
|
||||||
</button>
|
</button>
|
||||||
<nav class="pete-channel-nav flex min-w-0 items-center gap-1 sm:gap-2 overflow-x-auto rounded-full bg-[color:var(--card)] p-1 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<nav class="pete-channel-nav flex min-w-0 items-center gap-1 sm:gap-2 overflow-x-auto rounded-full bg-card p-1 shadow-pete border-2 border-ink/10">
|
||||||
{{$active := .Active}}
|
{{$active := .Active}}
|
||||||
{{if .User}}
|
{{if .User}}
|
||||||
<a href="/for-you"
|
<a href="/for-you"
|
||||||
title="Your personalized feed"
|
title="Your personalized feed"
|
||||||
class="shrink-0 rounded-full px-3 py-2 text-sm font-semibold transition
|
class="shrink-0 rounded-full px-3 py-2 text-sm font-semibold transition
|
||||||
{{if eq $active "for-you"}}bg-[color:var(--accent)] text-[#1c1305] shadow-sm{{else}}hover:bg-[color:var(--ink)]/5{{end}}">
|
{{if eq $active "for-you"}}bg-accent text-[#1c1305] shadow-sm{{else}}hover:bg-ink/5{{end}}">
|
||||||
<span aria-hidden="true" class="mr-1">✨</span><span class="hidden sm:inline">For you</span>
|
<span aria-hidden="true" class="mr-1">✨</span><span class="hidden sm:inline">For you</span>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{range .Channels}}
|
{{range .Channels}}
|
||||||
<a href="/{{.Slug}}"
|
<a href="/{{.Slug}}"
|
||||||
class="shrink-0 rounded-full px-3 py-2 text-sm font-semibold transition
|
class="shrink-0 rounded-full px-3 py-2 text-sm font-semibold transition
|
||||||
{{if eq $active .Slug}}bg-theme-{{.Theme}} text-white shadow-sm{{else}}hover:bg-[color:var(--ink)]/5{{end}}">
|
{{if eq $active .Slug}}bg-theme-{{.Theme}} text-white shadow-sm{{else}}hover:bg-ink/5{{end}}">
|
||||||
<span aria-hidden="true" class="mr-1">{{.Emoji}}</span><span class="hidden sm:inline">{{.Title}}</span>
|
<span aria-hidden="true" class="mr-1">{{.Emoji}}</span><span class="hidden sm:inline">{{.Title}}</span>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -167,37 +167,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="pete-search" class="hidden fixed inset-0 z-50 bg-[color:var(--ink)]/40 backdrop-blur-sm flex items-start justify-center pt-[10vh] px-4" aria-modal="true" role="dialog">
|
<div id="pete-search" class="hidden fixed inset-0 z-50 bg-ink/40 backdrop-blur-sm flex items-start justify-center pt-[10vh] px-4" aria-modal="true" role="dialog">
|
||||||
<div class="w-full max-w-2xl rounded-3xl bg-[color:var(--card)] shadow-pete-lg border-2 border-[color:var(--ink)]/10 overflow-hidden">
|
<div class="w-full max-w-2xl rounded-3xl bg-card shadow-pete-lg border-2 border-ink/10 overflow-hidden">
|
||||||
<div class="flex items-center gap-3 px-5 py-4 border-b border-[color:var(--ink)]/10">
|
<div class="flex items-center gap-3 px-5 py-4 border-b border-ink/10">
|
||||||
<kbd class="hidden sm:inline-flex items-center rounded-md bg-[color:var(--ink)]/5 px-1.5 py-0.5 text-[10px] font-mono font-semibold text-[color:var(--ink)]/60">⌘K</kbd>
|
<kbd class="hidden sm:inline-flex items-center rounded-md bg-ink/5 px-1.5 py-0.5 text-[10px] font-mono font-semibold text-muted">⌘K</kbd>
|
||||||
<input type="search" data-search-input autocomplete="off" spellcheck="false"
|
<input type="search" data-search-input autocomplete="off" spellcheck="false"
|
||||||
placeholder="Search headlines…"
|
placeholder="Search headlines…"
|
||||||
class="flex-1 bg-transparent text-lg outline-none placeholder:text-[color:var(--ink)]/40">
|
class="flex-1 bg-transparent text-lg outline-none placeholder:text-muted">
|
||||||
</div>
|
</div>
|
||||||
<div data-search-list class="max-h-[60vh] overflow-y-auto p-3 space-y-1"></div>
|
<div data-search-list class="max-h-[60vh] overflow-y-auto p-3 space-y-1"></div>
|
||||||
<div class="px-5 py-3 border-t border-[color:var(--ink)]/10 flex items-center justify-between text-xs text-[color:var(--ink)]/50">
|
<div class="px-5 py-3 border-t border-ink/10 flex items-center justify-between text-xs text-muted">
|
||||||
<span><kbd class="font-mono">↑↓</kbd> navigate · <kbd class="font-mono">⏎</kbd> open · <kbd class="font-mono">esc</kbd> close</span>
|
<span><kbd class="font-mono">↑↓</kbd> navigate · <kbd class="font-mono">⏎</kbd> open · <kbd class="font-mono">esc</kbd> close</span>
|
||||||
<span data-search-meta></span>
|
<span data-search-meta></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="pete-settings" class="hidden fixed inset-0 z-50 bg-[color:var(--ink)]/40 backdrop-blur-sm flex items-start justify-center pt-[10vh] px-4" aria-modal="true" role="dialog">
|
<div id="pete-settings" class="hidden fixed inset-0 z-50 bg-ink/40 backdrop-blur-sm flex items-start justify-center pt-[10vh] px-4" aria-modal="true" role="dialog">
|
||||||
<div class="w-full max-w-md rounded-3xl bg-[color:var(--card)] shadow-pete-lg border-2 border-[color:var(--ink)]/10 overflow-hidden">
|
<div class="w-full max-w-md rounded-3xl bg-card shadow-pete-lg border-2 border-ink/10 overflow-hidden">
|
||||||
<div class="flex items-center justify-between gap-3 px-5 py-4 border-b border-[color:var(--ink)]/10">
|
<div class="flex items-center justify-between gap-3 px-5 py-4 border-b border-ink/10">
|
||||||
<h2 class="font-display text-lg font-bold">Feed settings</h2>
|
<h2 class="font-display text-lg font-bold">Feed settings</h2>
|
||||||
<button type="button" data-settings-close class="rounded-full px-2 py-1 text-sm text-[color:var(--ink)]/60 hover:bg-[color:var(--ink)]/5">esc</button>
|
<button type="button" data-settings-close class="rounded-full px-2 py-1 text-sm text-muted hover:bg-ink/5">esc</button>
|
||||||
</div>
|
</div>
|
||||||
{{if .PushEnabled}}{{if .User}}
|
{{if .PushEnabled}}{{if .User}}
|
||||||
<div data-push-section hidden class="px-5 pt-4 pb-1">
|
<div data-push-section hidden class="px-5 pt-4 pb-1">
|
||||||
<div class="flex items-center justify-between gap-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-3">
|
<div class="flex items-center justify-between gap-3 rounded-2xl bg-ink/5 px-4 py-3">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="text-sm font-bold">🔔 Notifications</div>
|
<div class="text-sm font-bold">🔔 Notifications</div>
|
||||||
<div data-push-note class="text-xs text-[color:var(--ink)]/60">Get a nudge when new stories land.</div>
|
<div data-push-note class="text-xs text-muted">Get a nudge when new stories land.</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" data-push-toggle aria-pressed="false"
|
<button type="button" data-push-toggle aria-pressed="false"
|
||||||
class="shrink-0 rounded-full bg-[color:var(--accent)] px-3 py-2 text-sm font-semibold text-[#1c1305] shadow-sm hover:brightness-105 transition disabled:opacity-50 disabled:cursor-not-allowed">
|
class="shrink-0 rounded-full bg-accent px-3 py-2 text-sm font-semibold text-[#1c1305] shadow-sm hover:brightness-105 transition disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
Turn on notifications
|
Turn on notifications
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,37 +206,37 @@
|
|||||||
because a category toggle with no subscription behind it is a switch
|
because a category toggle with no subscription behind it is a switch
|
||||||
wired to nothing. Every box starts unchecked: turning on news
|
wired to nothing. Every box starts unchecked: turning on news
|
||||||
notifications is not consent to be told about the game. -->
|
notifications is not consent to be told about the game. -->
|
||||||
<div data-adv-push hidden class="mt-2 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-3">
|
<div data-adv-push hidden class="mt-2 rounded-2xl bg-ink/5 px-4 py-3">
|
||||||
<div class="text-sm font-bold">⚔️ Adventure alerts</div>
|
<div class="text-sm font-bold">⚔️ Adventure alerts</div>
|
||||||
<div class="text-xs text-[color:var(--ink)]/60">Pick what's worth a buzz. Off unless you say so.</div>
|
<div class="text-xs text-muted">Pick what's worth a buzz. Off unless you say so.</div>
|
||||||
<div class="mt-2 space-y-1.5">
|
<div class="mt-2 space-y-1.5">
|
||||||
<label class="flex items-start gap-2 text-xs cursor-pointer">
|
<label class="flex items-start gap-2 text-xs cursor-pointer">
|
||||||
<input type="checkbox" data-adv-cat="siege" class="mt-0.5 accent-[color:var(--accent)]">
|
<input type="checkbox" data-adv-cat="siege" class="mt-0.5 accent-accent">
|
||||||
<span><span class="font-semibold">The Siege</span>: when a world boss camps outside town, and when it's settled.</span>
|
<span><span class="font-semibold">The Siege</span>: when a world boss camps outside town, and when it's settled.</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-start gap-2 text-xs cursor-pointer">
|
<label class="flex items-start gap-2 text-xs cursor-pointer">
|
||||||
<input type="checkbox" data-adv-cat="run" class="mt-0.5 accent-[color:var(--accent)]">
|
<input type="checkbox" data-adv-cat="run" class="mt-0.5 accent-accent">
|
||||||
<span><span class="font-semibold">Your expeditions</span>: cleared, backed out, or worse.</span>
|
<span><span class="font-semibold">Your expeditions</span>: cleared, backed out, or worse.</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-start gap-2 text-xs cursor-pointer">
|
<label class="flex items-start gap-2 text-xs cursor-pointer">
|
||||||
<input type="checkbox" data-adv-cat="departure" class="mt-0.5 accent-[color:var(--accent)]">
|
<input type="checkbox" data-adv-cat="departure" class="mt-0.5 accent-accent">
|
||||||
<span><span class="font-semibold">Wandering off</span>: your adventurer got bored and left without you.</span>
|
<span><span class="font-semibold">Wandering off</span>: your adventurer got bored and left without you.</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-start gap-2 text-xs cursor-pointer">
|
<label class="flex items-start gap-2 text-xs cursor-pointer">
|
||||||
<input type="checkbox" data-adv-cat="contract" class="mt-0.5 accent-[color:var(--accent)]">
|
<input type="checkbox" data-adv-cat="contract" class="mt-0.5 accent-accent">
|
||||||
<span><span class="font-semibold">Contracts on you</span>: somebody paid to have something sent after you.</span>
|
<span><span class="font-semibold">Contracts on you</span>: somebody paid to have something sent after you.</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div data-adv-push-note class="mt-2 text-xs text-[color:var(--ink)]/50"></div>
|
<div data-adv-push-note class="mt-2 text-xs text-muted"></div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}{{end}}
|
{{end}}{{end}}
|
||||||
<p class="px-5 pt-3 text-xs text-[color:var(--ink)]/60">Uncheck a feed to hide its stories. <span data-storage-note>Saved in this browser.</span></p>
|
<p class="px-5 pt-3 text-xs text-muted">Uncheck a feed to hide its stories. <span data-storage-note>Saved in this browser.</span></p>
|
||||||
<div data-settings-list class="max-h-[55vh] overflow-y-auto p-3 space-y-1"></div>
|
<div data-settings-list class="max-h-[55vh] overflow-y-auto p-3 space-y-1"></div>
|
||||||
<div class="px-5 py-3 border-t border-[color:var(--ink)]/10 flex items-center justify-between text-xs">
|
<div class="px-5 py-3 border-t border-ink/10 flex items-center justify-between text-xs">
|
||||||
<button type="button" data-settings-reset class="text-[color:var(--ink)]/60 hover:text-[color:var(--ink)] font-semibold">Enable all</button>
|
<button type="button" data-settings-reset class="text-muted hover:text-ink font-semibold">Enable all</button>
|
||||||
<span data-settings-meta class="text-[color:var(--ink)]/50"></span>
|
<span data-settings-meta class="text-muted"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -302,8 +302,8 @@
|
|||||||
{{block "main" .}}{{end}}
|
{{block "main" .}}{{end}}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="mx-auto max-w-6xl px-4 pb-10 text-center text-sm text-[color:var(--ink)]/50">
|
<footer class="mx-auto max-w-6xl px-4 pb-10 text-center text-sm text-muted">
|
||||||
<img src="/static/img/pete.avif" alt="" width="20" height="20"
|
<img src="{{asset "/static/img/pete.avif"}}" alt="" width="20" height="20"
|
||||||
class="inline-block h-5 w-5 rounded-full object-cover align-[-4px] mr-1">
|
class="inline-block h-5 w-5 rounded-full object-cover align-[-4px] mr-1">
|
||||||
served by <span class="font-semibold">Pete</span> · also a Matrix bot · <span data-clock>—</span>
|
served by <span class="font-semibold">Pete</span> · also a Matrix bot · <span data-clock>—</span>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -342,15 +342,15 @@
|
|||||||
window.PETE_PUSH = {{if .PushEnabled}}{ enabled: true, publicKey: {{.PushPublicKey}} }{{else}}null{{end}};
|
window.PETE_PUSH = {{if .PushEnabled}}{ enabled: true, publicKey: {{.PushPublicKey}} }{{else}}null{{end}};
|
||||||
window.PETE_TTS = {{.TTS}};
|
window.PETE_TTS = {{.TTS}};
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/js/prefs.js" defer></script>
|
<script src="{{asset "/static/js/prefs.js"}}" defer></script>
|
||||||
<script src="/static/js/weather-2d.js" defer></script>
|
<script src="{{asset "/static/js/weather-2d.js"}}" defer></script>
|
||||||
<script src="/static/js/weather-gl.js" defer></script>
|
<script src="{{asset "/static/js/weather-gl.js"}}" defer></script>
|
||||||
<script src="/static/js/weather.js" defer></script>
|
<script src="{{asset "/static/js/weather.js"}}" defer></script>
|
||||||
<script src="/static/js/weather-forecast.js" defer></script>
|
<script src="{{asset "/static/js/weather-forecast.js"}}" defer></script>
|
||||||
<script src="/static/js/search.js" defer></script>
|
<script src="{{asset "/static/js/search.js"}}" defer></script>
|
||||||
<script src="/static/js/settings.js" defer></script>
|
<script src="{{asset "/static/js/settings.js"}}" defer></script>
|
||||||
<script src="/static/js/reader.js" defer></script>
|
<script src="{{asset "/static/js/reader.js"}}" defer></script>
|
||||||
<script src="/static/js/pwa.js" defer></script>
|
<script src="{{asset "/static/js/pwa.js"}}" defer></script>
|
||||||
{{block "scripts" .}}{{end}}
|
{{block "scripts" .}}{{end}}
|
||||||
</body>
|
</body>
|
||||||
</html>{{end}}
|
</html>{{end}}
|
||||||
|
|||||||
@@ -8,39 +8,39 @@
|
|||||||
<li class="realm-zone{{if .Unbeaten}} realm-zone-unbeaten{{end}}{{if .Busy}} realm-zone-busy{{end}}">
|
<li class="realm-zone{{if .Unbeaten}} realm-zone-unbeaten{{end}}{{if .Busy}} realm-zone-busy{{end}}">
|
||||||
<div class="flex items-baseline justify-between gap-3">
|
<div class="flex items-baseline justify-between gap-3">
|
||||||
<h3 class="font-display font-bold text-base flex-1 min-w-0 truncate">{{.Display}}</h3>
|
<h3 class="font-display font-bold text-base flex-1 min-w-0 truncate">{{.Display}}</h3>
|
||||||
{{if .Levels}}<span class="text-[11px] uppercase tracking-wider text-[color:var(--ink)]/40 shrink-0">{{.Levels}}</span>{{end}}
|
{{if .Levels}}<span class="text-[11px] uppercase tracking-wider text-muted shrink-0">{{.Levels}}</span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .Atmosphere}}
|
{{if .Atmosphere}}
|
||||||
<p class="mt-1 text-xs text-[color:var(--ink)]/55 leading-relaxed">{{.Atmosphere}}</p>
|
<p class="mt-1 text-xs text-muted leading-relaxed">{{.Atmosphere}}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="mt-2.5 text-xs">
|
<div class="mt-2.5 text-xs">
|
||||||
{{if .Unbeaten}}
|
{{if .Unbeaten}}
|
||||||
<p class="realm-unbeaten-line">Nobody has ever come back out of it.</p>
|
<p class="realm-unbeaten-line">Nobody has ever come back out of it.</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="text-[color:var(--ink)]/60">
|
<p class="text-muted">
|
||||||
{{/* FirstClearBy empty with Clears > 0 is the anonymised case: the zone
|
{{/* FirstClearBy empty with Clears > 0 is the anonymised case: the zone
|
||||||
has been beaten, the clearer opted out. It must not read as unbeaten. */}}
|
has been beaten, the clearer opted out. It must not read as unbeaten. */}}
|
||||||
First through:
|
First through:
|
||||||
{{if .FirstClearToken}}<a href="/adventure/who/{{.FirstClearToken}}" class="font-semibold hover:text-theme-adventure transition">{{.FirstClearBy}}</a>
|
{{if .FirstClearToken}}<a href="/adventure/who/{{.FirstClearToken}}" class="font-semibold hover:text-theme-adventure transition">{{.FirstClearBy}}</a>
|
||||||
{{else if .FirstClearBy}}<span class="font-semibold">{{.FirstClearBy}}</span>
|
{{else if .FirstClearBy}}<span class="font-semibold">{{.FirstClearBy}}</span>
|
||||||
{{else}}<span class="italic text-[color:var(--ink)]/45">somebody who'd rather not say</span>{{end}}
|
{{else}}<span class="italic text-muted">somebody who'd rather not say</span>{{end}}
|
||||||
{{if .FirstWhen}}<span class="text-[color:var(--ink)]/40"> · {{.FirstWhen}}</span>{{end}}
|
{{if .FirstWhen}}<span class="text-muted"> · {{.FirstWhen}}</span>{{end}}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-0.5 text-[color:var(--ink)]/45 tabular-nums">
|
<p class="mt-0.5 text-muted tabular-nums">
|
||||||
{{.Clears}} clear{{if ne .Clears 1}}s{{end}} by {{.Clearers}} adventurer{{if ne .Clearers 1}}s{{end}}
|
{{.Clears}} clear{{if ne .Clears 1}}s{{end}} by {{.Clearers}} adventurer{{if ne .Clearers 1}}s{{end}}
|
||||||
</p>
|
</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .Occupants}}
|
{{if .Occupants}}
|
||||||
<div class="mt-2.5 pt-2.5 border-t border-[color:var(--ink)]/10">
|
<div class="mt-2.5 pt-2.5 border-t border-ink/10">
|
||||||
<p class="text-[11px] uppercase tracking-wider text-theme-adventure font-semibold">In there now</p>
|
<p class="text-[11px] uppercase tracking-wider text-theme-adventure font-semibold">In there now</p>
|
||||||
<p class="mt-1 text-xs">
|
<p class="mt-1 text-xs">
|
||||||
{{range $i, $o := .Occupants}}{{if $i}}<span class="text-[color:var(--ink)]/30">, </span>{{end}}<!--
|
{{range $i, $o := .Occupants}}{{if $i}}<span class="text-ink/30">, </span>{{end}}<!--
|
||||||
-->{{if $o.Token}}<a href="/adventure/who/{{$o.Token}}" class="font-semibold hover:text-theme-adventure transition">{{$o.Name}}</a>{{else}}<span class="font-semibold">{{$o.Name}}</span>{{end}}<!--
|
-->{{if $o.Token}}<a href="/adventure/who/{{$o.Token}}" class="font-semibold hover:text-theme-adventure transition">{{$o.Name}}</a>{{else}}<span class="font-semibold">{{$o.Name}}</span>{{end}}<!--
|
||||||
-->{{if $o.Day}}<span class="text-[color:var(--ink)]/40"> (day {{$o.Day}})</span>{{end}}{{end}}
|
-->{{if $o.Day}}<span class="text-muted"> (day {{$o.Day}})</span>{{end}}{{end}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -80,8 +80,8 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{{if not .Realm.Known}}
|
{{if not .Realm.Known}}
|
||||||
<div class="mt-6 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
<div class="mt-6 rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete">
|
||||||
<p class="text-sm text-[color:var(--ink)]/60">
|
<p class="text-sm text-muted">
|
||||||
I haven't had the survey through from the field yet. When it lands, the whole
|
I haven't had the survey through from the field yet. When it lands, the whole
|
||||||
world lives on this page.
|
world lives on this page.
|
||||||
</p>
|
</p>
|
||||||
@@ -92,11 +92,11 @@
|
|||||||
<section class="mt-8{{if .Postgame}} realm-band-postgame rounded-3xl p-5 sm:p-6{{end}}">
|
<section class="mt-8{{if .Postgame}} realm-band-postgame rounded-3xl p-5 sm:p-6{{end}}">
|
||||||
<div class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 mb-1">
|
<div class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 mb-1">
|
||||||
<h2 class="font-display text-xl sm:text-2xl font-bold">{{.Label}}</h2>
|
<h2 class="font-display text-xl sm:text-2xl font-bold">{{.Label}}</h2>
|
||||||
<span class="text-xs text-[color:var(--ink)]/45 tabular-nums shrink-0">
|
<span class="text-xs text-muted tabular-nums shrink-0">
|
||||||
{{.Cleared}} of {{len .Zones}} beaten
|
{{.Cleared}} of {{len .Zones}} beaten
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{{if .Blurb}}<p class="text-sm text-[color:var(--ink)]/55 mb-4 max-w-2xl">{{.Blurb}}</p>{{end}}
|
{{if .Blurb}}<p class="text-sm text-muted mb-4 max-w-2xl">{{.Blurb}}</p>{{end}}
|
||||||
<ul class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<ul class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{{range .Zones}}{{template "realmzone" .}}{{end}}
|
{{range .Zones}}{{template "realmzone" .}}{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -104,8 +104,8 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .Realm.Known}}
|
{{if .Realm.Known}}
|
||||||
<p class="mt-8 text-xs text-[color:var(--ink)]/40 text-center">
|
<p class="mt-8 text-xs text-muted text-center">
|
||||||
Say <code class="rounded bg-[color:var(--ink)]/8 px-1.5 py-0.5 font-mono">!expedition start</code>
|
Say <code class="rounded bg-ink/[.08] px-1.5 py-0.5 font-mono">!expedition start</code>
|
||||||
to me in Matrix to pick one and go.
|
to me in Matrix to pick one and go.
|
||||||
</p>
|
</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
<article class="mt-2 mb-10 max-w-3xl mx-auto">
|
<article class="mt-2 mb-10 max-w-3xl mx-auto">
|
||||||
<nav class="mb-4 flex items-center gap-4">
|
<nav class="mb-4 flex items-center gap-4">
|
||||||
<a href="/adventure" class="inline-flex items-center gap-1.5 text-sm font-semibold text-[color:var(--ink)]/60 hover:text-[color:var(--ink)] transition">
|
<a href="/adventure" class="inline-flex items-center gap-1.5 text-sm font-semibold text-muted hover:text-ink transition">
|
||||||
<span aria-hidden="true">←</span> All dispatches
|
<span aria-hidden="true">←</span> All dispatches
|
||||||
</a>
|
</a>
|
||||||
{{if .WhoURL}}
|
{{if .WhoURL}}
|
||||||
<a href="{{.WhoURL}}" class="inline-flex items-center gap-1.5 text-sm font-semibold text-[color:var(--ink)]/60 hover:text-[color:var(--ink)] transition">
|
<a href="{{.WhoURL}}" class="inline-flex items-center gap-1.5 text-sm font-semibold text-muted hover:text-ink transition">
|
||||||
{{.Name}}'s sheet <span aria-hidden="true">→</span>
|
{{.Name}}'s sheet <span aria-hidden="true">→</span>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -29,17 +29,17 @@
|
|||||||
happened: the game's own model reading the finished run back. Guarded at
|
happened: the game's own model reading the finished run back. Guarded at
|
||||||
ingest, and absent entirely when the model is off — which is why it sits
|
ingest, and absent entirely when the model is off — which is why it sits
|
||||||
above the numbers rather than instead of them. -->
|
above the numbers rather than instead of them. -->
|
||||||
<section class="mt-6 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 sm:p-8 shadow-pete">
|
<section class="mt-6 rounded-3xl bg-card border-2 border-ink/10 p-6 sm:p-8 shadow-pete">
|
||||||
<p class="text-lg leading-relaxed text-[color:var(--ink)]/90 whitespace-pre-line">{{.Summary}}</p>
|
<p class="text-lg leading-relaxed text-ink/90 whitespace-pre-line">{{.Summary}}</p>
|
||||||
</section>
|
</section>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .Stats}}
|
{{if .Stats}}
|
||||||
<section class="mt-6 grid grid-cols-2 sm:grid-cols-3 gap-2">
|
<section class="mt-6 grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||||
{{range .Stats}}
|
{{range .Stats}}
|
||||||
<div class="rounded-2xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 px-2 py-3 text-center shadow-pete">
|
<div class="rounded-2xl bg-card border-2 border-ink/10 px-2 py-3 text-center shadow-pete">
|
||||||
<div class="font-display text-2xl font-bold leading-none tabular-nums">{{.Value}}</div>
|
<div class="font-display text-2xl font-bold leading-none tabular-nums">{{.Value}}</div>
|
||||||
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50 mt-1.5">{{.Label}}</div>
|
<div class="text-[10px] uppercase tracking-wider text-muted mt-1.5">{{.Label}}</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</section>
|
</section>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
{{if .Turning}}
|
{{if .Turning}}
|
||||||
<!-- The worst single thing that happened, pulled out of the middle of the log
|
<!-- The worst single thing that happened, pulled out of the middle of the log
|
||||||
where it would otherwise read as one line among forty. -->
|
where it would otherwise read as one line among forty. -->
|
||||||
<section class="mt-6 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
<section class="mt-6 rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete">
|
||||||
<h2 class="font-display text-xl font-bold mb-3">Where it turned</h2>
|
<h2 class="font-display text-xl font-bold mb-3">Where it turned</h2>
|
||||||
<p class="flex items-baseline gap-2.5">
|
<p class="flex items-baseline gap-2.5">
|
||||||
<span aria-hidden="true">{{.Turning.Emoji}}</span>
|
<span aria-hidden="true">{{.Turning.Emoji}}</span>
|
||||||
@@ -58,10 +58,10 @@
|
|||||||
</section>
|
</section>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<section class="mt-6 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
<section class="mt-6 rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete">
|
||||||
<div class="flex items-baseline justify-between mb-4 gap-3">
|
<div class="flex items-baseline justify-between mb-4 gap-3">
|
||||||
<h2 class="font-display text-xl font-bold">Room by room</h2>
|
<h2 class="font-display text-xl font-bold">Room by room</h2>
|
||||||
{{if .Live}}<span class="text-sm text-[color:var(--ink)]/50">still under way</span>{{end}}
|
{{if .Live}}<span class="text-sm text-muted">still under way</span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
<ol class="runlog runlog-full">
|
<ol class="runlog runlog-full">
|
||||||
{{range .Lines}}
|
{{range .Lines}}
|
||||||
@@ -73,9 +73,9 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</ol>
|
</ol>
|
||||||
{{if .Truncated}}
|
{{if .Truncated}}
|
||||||
<p class="mt-4 text-xs text-[color:var(--ink)]/45">Only the last {{len .Lines}} moments of this run are shown — it beat out more than the report keeps.</p>
|
<p class="mt-4 text-xs text-muted">Only the last {{len .Lines}} moments of this run are shown — it beat out more than the report keeps.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
<p class="mt-8 pt-6 border-t border-[color:var(--ink)]/10 text-sm italic text-theme-adventure font-semibold">
|
<p class="mt-8 pt-6 border-t border-ink/10 text-sm italic text-theme-adventure font-semibold">
|
||||||
Reporting from the realm, this is Pete.
|
Reporting from the realm, this is Pete.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -5,16 +5,16 @@
|
|||||||
carries no token, so they get no link — their damage still counts and still
|
carries no token, so they get no link — their damage still counts and still
|
||||||
holds its rank, they just aren't named. */}}
|
holds its rank, they just aren't named. */}}
|
||||||
{{define "defenderrow"}}
|
{{define "defenderrow"}}
|
||||||
<li class="flex items-baseline gap-3 py-1.5 border-b border-[color:var(--ink)]/5 last:border-0">
|
<li class="flex items-baseline gap-3 py-1.5 border-b border-ink/5 last:border-0">
|
||||||
<span class="flex-1 min-w-0">
|
<span class="flex-1 min-w-0">
|
||||||
{{if .Token}}<a href="/adventure/who/{{.Token}}" class="font-semibold hover:text-theme-adventure transition truncate">{{.Name}}</a>
|
{{if .Token}}<a href="/adventure/who/{{.Token}}" class="font-semibold hover:text-theme-adventure transition truncate">{{.Name}}</a>
|
||||||
{{else}}<span class="font-semibold text-[color:var(--ink)]/55 italic truncate">{{.Name}}</span>{{end}}
|
{{else}}<span class="font-semibold text-muted italic truncate">{{.Name}}</span>{{end}}
|
||||||
{{if .Level}}<span class="text-xs text-[color:var(--ink)]/40 ml-1.5">lv {{.Level}}</span>{{end}}
|
{{if .Level}}<span class="text-xs text-muted ml-1.5">lv {{.Level}}</span>{{end}}
|
||||||
</span>
|
</span>
|
||||||
{{if .Fights}}
|
{{if .Fights}}
|
||||||
<span class="text-xs text-[color:var(--ink)]/50 shrink-0 tabular-nums">{{.Damage}} dmg · {{.Fights}} bout{{if ne .Fights 1}}s{{end}}</span>
|
<span class="text-xs text-muted shrink-0 tabular-nums">{{.Damage}} dmg · {{.Fights}} bout{{if ne .Fights 1}}s{{end}}</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="text-xs text-[color:var(--ink)]/35 shrink-0">not yet in it</span>
|
<span class="text-xs text-muted shrink-0">not yet in it</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -62,10 +62,10 @@
|
|||||||
Matrix command stays on the page for everyone else, because it is still
|
Matrix command stays on the page for everyone else, because it is still
|
||||||
the only door for a visitor who isn't signed in — and the blow-by-blow of
|
the only door for a visitor who isn't signed in — and the blow-by-blow of
|
||||||
the fight arrives there whichever door you came through. -->
|
the fight arrives there whichever door you came through. -->
|
||||||
<div id="adv-actions" class="adv-actions mt-6 rounded-3xl bg-[color:var(--card)] border-2 border-theme-adventure/30 p-5 shadow-pete">
|
<div id="adv-actions" class="adv-actions mt-6 rounded-3xl bg-card border-2 border-theme-adventure/30 p-5 shadow-pete">
|
||||||
{{if .YouOnBoard}}
|
{{if .YouOnBoard}}
|
||||||
{{if .YouFought}}
|
{{if .YouFought}}
|
||||||
<p class="text-sm text-[color:var(--ink)]/75">
|
<p class="text-sm text-ink/75">
|
||||||
<span class="font-semibold text-theme-adventure">You've taken your bout today.</span>
|
<span class="font-semibold text-theme-adventure">You've taken your bout today.</span>
|
||||||
Come back tomorrow. One fight each, per day, and everyone in the right-hand column below still has theirs.
|
Come back tomorrow. One fight each, per day, and everyone in the right-hand column below still has theirs.
|
||||||
</p>
|
</p>
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
the page can't go on saying "unspent" over a fight that just
|
the page can't go on saying "unspent" over a fight that just
|
||||||
happened. */}}
|
happened. */}}
|
||||||
<div data-offer>
|
<div data-offer>
|
||||||
<p class="text-sm text-[color:var(--ink)]/75 mb-3">
|
<p class="text-sm text-ink/75 mb-3">
|
||||||
<span class="font-semibold text-theme-adventure">Your bout is unspent.</span>
|
<span class="font-semibold text-theme-adventure">Your bout is unspent.</span>
|
||||||
Damage counts whether you win the fight or not. Turning up is the mechanic, and the blow-by-blow lands in Matrix.
|
Damage counts whether you win the fight or not. Turning up is the mechanic, and the blow-by-blow lands in Matrix.
|
||||||
</p>
|
</p>
|
||||||
@@ -88,9 +88,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="text-sm text-[color:var(--ink)]/75">
|
<p class="text-sm text-ink/75">
|
||||||
<span class="font-semibold text-theme-adventure">Taking your bout:</span>
|
<span class="font-semibold text-theme-adventure">Taking your bout:</span>
|
||||||
say <code class="rounded bg-[color:var(--ink)]/8 px-1.5 py-0.5 font-mono text-xs">!adventure siege fight</code>
|
say <code class="rounded bg-ink/[.08] px-1.5 py-0.5 font-mono text-xs">!adventure siege fight</code>
|
||||||
to me in Matrix. One a day, each. Damage counts whether you win the fight or not; turning up is the mechanic.
|
to me in Matrix. One a day, each. Damage counts whether you win the fight or not; turning up is the mechanic.
|
||||||
</p>
|
</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
<!-- The muster. Two columns, and the right-hand one is the point: a bout not
|
<!-- The muster. Two columns, and the right-hand one is the point: a bout not
|
||||||
taken today is damage the pool never sees. -->
|
taken today is damage the pool never sees. -->
|
||||||
<section class="mt-6 grid gap-6 sm:grid-cols-2">
|
<section class="mt-6 grid gap-6 sm:grid-cols-2">
|
||||||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
<div class="rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete">
|
||||||
<div class="flex items-baseline justify-between mb-3">
|
<div class="flex items-baseline justify-between mb-3">
|
||||||
<h2 class="font-display text-xl font-bold">In it today</h2>
|
<h2 class="font-display text-xl font-bold">In it today</h2>
|
||||||
<span class="siege-chip siege-chip-fought">{{len .Siege.Fought}}</span>
|
<span class="siege-chip siege-chip-fought">{{len .Siege.Fought}}</span>
|
||||||
@@ -111,31 +111,31 @@
|
|||||||
{{if .Siege.Fought}}
|
{{if .Siege.Fought}}
|
||||||
<ul class="text-sm">{{range .Siege.Fought}}{{template "defenderrow" .}}{{end}}</ul>
|
<ul class="text-sm">{{range .Siege.Fought}}{{template "defenderrow" .}}{{end}}</ul>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="text-sm text-[color:var(--ink)]/50">Nobody's swung at it yet today. The pool doesn't move on its own.</p>
|
<p class="text-sm text-muted">Nobody's swung at it yet today. The pool doesn't move on its own.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
<div class="rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete">
|
||||||
<div class="flex items-baseline justify-between mb-3">
|
<div class="flex items-baseline justify-between mb-3">
|
||||||
<h2 class="font-display text-xl font-bold">Bout still going spare</h2>
|
<h2 class="font-display text-xl font-bold">Bout still going spare</h2>
|
||||||
<span class="siege-chip siege-chip-waiting">{{len .Siege.Waiting}}</span>
|
<span class="siege-chip siege-chip-waiting">{{len .Siege.Waiting}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{if .Siege.Waiting}}
|
{{if .Siege.Waiting}}
|
||||||
<ul class="text-sm">{{range .Siege.Waiting}}{{template "defenderrow" .}}{{end}}</ul>
|
<ul class="text-sm">{{range .Siege.Waiting}}{{template "defenderrow" .}}{{end}}</ul>
|
||||||
<p class="mt-3 text-xs text-[color:var(--ink)]/45">Each of these is a free hit the town hasn't taken.</p>
|
<p class="mt-3 text-xs text-muted">Each of these is a free hit the town hasn't taken.</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="text-sm text-[color:var(--ink)]/50">Everyone's been out. Good turnout.</p>
|
<p class="text-sm text-muted">Everyone's been out. Good turnout.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{{else}}
|
{{else}}
|
||||||
<header class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 sm:p-10 shadow-pete relative overflow-hidden">
|
<header class="rounded-3xl bg-card border-2 border-ink/10 p-6 sm:p-10 shadow-pete relative overflow-hidden">
|
||||||
<div class="absolute -top-8 -right-4 text-[12rem] opacity-10 select-none" aria-hidden="true">🏰</div>
|
<div class="absolute -top-8 -right-4 text-[12rem] opacity-10 select-none" aria-hidden="true">🏰</div>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<p class="text-sm uppercase tracking-[0.2em] text-[color:var(--ink)]/50">🏰 The Siege</p>
|
<p class="text-sm uppercase tracking-[0.2em] text-muted">🏰 The Siege</p>
|
||||||
<h1 class="font-display text-3xl sm:text-4xl font-bold mt-2 leading-tight">Nothing's camped outside town.</h1>
|
<h1 class="font-display text-3xl sm:text-4xl font-bold mt-2 leading-tight">Nothing's camped outside town.</h1>
|
||||||
<p class="mt-3 text-[color:var(--ink)]/70">
|
<p class="mt-3 text-muted">
|
||||||
{{if .Siege.Known}}
|
{{if .Siege.Known}}
|
||||||
Quiet month so far. One comes for the town every month — a named thing with a shared health pool, and everybody gets a swing a day at it.
|
Quiet month so far. One comes for the town every month — a named thing with a shared health pool, and everybody gets a swing a day at it.
|
||||||
{{else}}
|
{{else}}
|
||||||
@@ -150,21 +150,21 @@
|
|||||||
<!-- The history is what makes the live bar mean something. A won siege ends
|
<!-- The history is what makes the live bar mean something. A won siege ends
|
||||||
at zero and draws as an empty track; a lost one shows exactly how much
|
at zero and draws as an empty track; a lost one shows exactly how much
|
||||||
was left standing when the window shut. -->
|
was left standing when the window shut. -->
|
||||||
<section class="mt-8 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
<section class="mt-8 rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete">
|
||||||
<h2 class="font-display text-xl font-bold mb-4">Sieges past</h2>
|
<h2 class="font-display text-xl font-bold mb-4">Sieges past</h2>
|
||||||
<ul class="space-y-4">
|
<ul class="space-y-4">
|
||||||
{{range .Siege.History}}
|
{{range .Siege.History}}
|
||||||
<li>
|
<li>
|
||||||
<div class="flex items-baseline justify-between gap-3">
|
<div class="flex items-baseline justify-between gap-3">
|
||||||
<span class="font-semibold flex-1 min-w-0 truncate">{{.BossName}} <span class="text-xs font-normal text-[color:var(--ink)]/40">T{{.Tier}}</span></span>
|
<span class="font-semibold flex-1 min-w-0 truncate">{{.BossName}} <span class="text-xs font-normal text-muted">T{{.Tier}}</span></span>
|
||||||
<span class="siege-chip {{if .Won}}siege-chip-held{{else}}siege-chip-fell{{end}} shrink-0">{{if .Won}}town held{{else}}broke through{{end}}</span>
|
<span class="siege-chip {{if .Won}}siege-chip-held{{else}}siege-chip-fell{{end}} shrink-0">{{if .Won}}town held{{else}}broke through{{end}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1.5 siege-track siege-track-sm">
|
<div class="mt-1.5 siege-track siege-track-sm">
|
||||||
<div class="siege-fill {{if .Won}}siege-fill-spent{{end}}" style="width: {{.HPPercent}}%"></div>
|
<div class="siege-fill {{if .Won}}siege-fill-spent{{end}}" style="width: {{.HPPercent}}%"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1.5 flex flex-wrap items-baseline justify-between gap-x-3 text-xs text-[color:var(--ink)]/50">
|
<div class="mt-1.5 flex flex-wrap items-baseline justify-between gap-x-3 text-xs text-muted">
|
||||||
<span>
|
<span>
|
||||||
{{if .Won}}Felled by {{.Defenders}} defender{{if ne .Defenders 1}}s{{end}}{{else}}{{.HPRemaining}} HP still standing when the window shut{{end}}{{if .MVP}} · most bouts: <span class="font-semibold text-[color:var(--ink)]/70">{{.MVP}}</span>{{if .MVPFights}} ({{.MVPFights}}){{end}}{{end}}
|
{{if .Won}}Felled by {{.Defenders}} defender{{if ne .Defenders 1}}s{{end}}{{else}}{{.HPRemaining}} HP still standing when the window shut{{end}}{{if .MVP}} · most bouts: <span class="font-semibold text-muted">{{.MVP}}</span>{{if .MVPFights}} ({{.MVPFights}}){{end}}{{end}}
|
||||||
</span>
|
</span>
|
||||||
{{if .When}}<span class="shrink-0">{{.When}}</span>{{end}}
|
{{if .When}}<span class="shrink-0">{{.When}}</span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
@@ -239,4 +239,4 @@
|
|||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "scripts"}}<script src="/static/js/adventure-actions.js" defer></script>{{end}}
|
{{define "scripts"}}<script src="{{asset "/static/js/adventure-actions.js"}}" defer></script>{{end}}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div class="flex items-center gap-3 min-w-0">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
<a href="/games" class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-[color:var(--card)] shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition" title="Back to the casino">
|
<a href="/games" class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-card shadow-pete border-2 border-ink/10 hover:bg-ink/5 transition" title="Back to the casino">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5" aria-hidden="true">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5" aria-hidden="true">
|
||||||
<path d="M19 12H5"></path><polyline points="12 19 5 12 12 5"></polyline>
|
<path d="M19 12H5"></path><polyline points="12 19 5 12 12 5"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -13,14 +13,14 @@
|
|||||||
</a>
|
</a>
|
||||||
<h1 class="font-display text-3xl font-bold">Solitaire</h1>
|
<h1 class="font-display text-3xl font-bold">Solitaire</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-[color:var(--ink)]/50">You buy the deck. Every card you get home buys some of it back.</p>
|
<p class="text-sm text-muted">You buy the deck. Every card you get home buys some of it back.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{template "_chipbar" .}}
|
{{template "_chipbar" .}}
|
||||||
|
|
||||||
<!-- The felt. The board takes the room; the money lives in a rail down the
|
<!-- The felt. The board takes the room; the money lives in a rail down the
|
||||||
right of it, where the house rack can't collide with the foundations. -->
|
right of it, where the house rack can't collide with the foundations. -->
|
||||||
<section class="pete-felt pete-solitaire relative overflow-hidden rounded-3xl p-4 sm:p-6 lg:p-8 shadow-pete-lg border-2 border-[color:var(--ink)]/10">
|
<section class="pete-felt pete-solitaire relative overflow-hidden rounded-3xl p-4 sm:p-6 lg:p-8 shadow-pete-lg border-2 border-ink/10">
|
||||||
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr),auto] lg:gap-8">
|
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr),auto] lg:gap-8">
|
||||||
|
|
||||||
<!-- The board. -->
|
<!-- The board. -->
|
||||||
@@ -85,22 +85,22 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Playing: shown while a board is live. -->
|
<!-- Playing: shown while a board is live. -->
|
||||||
<section data-playing class="hidden rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section data-playing class="hidden rounded-3xl bg-card p-5 sm:p-6 shadow-pete border-2 border-ink/10">
|
||||||
|
|
||||||
<!-- The ordinary controls, while there's still a game to play. -->
|
<!-- The ordinary controls, while there's still a game to play. -->
|
||||||
<div data-play-controls class="flex flex-wrap items-center gap-3">
|
<div data-play-controls class="flex flex-wrap items-center gap-3">
|
||||||
<button type="button" data-auto
|
<button type="button" data-auto
|
||||||
class="rounded-full bg-[color:var(--ink)]/5 px-5 py-2.5 font-display font-bold border-2 border-[color:var(--ink)]/10
|
class="rounded-full bg-ink/5 px-5 py-2.5 font-display font-bold border-2 border-ink/10
|
||||||
hover:bg-[color:var(--ink)]/10 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:bg-ink/10 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Send home ⤴
|
Send home ⤴
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p class="text-xs text-[color:var(--ink)]/45">
|
<p class="text-xs text-muted">
|
||||||
Drag a card where it goes, or click it and then click the spot. Double-click sends it home.
|
Drag a card where it goes, or click it and then click the spot. Double-click sends it home.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button type="button" data-cash
|
<button type="button" data-cash
|
||||||
class="ml-auto rounded-full bg-[color:var(--accent)] px-6 py-2.5 font-display font-bold text-white shadow-pete
|
class="ml-auto rounded-full bg-accent px-6 py-2.5 font-display font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Cash the board · <span data-cash-amount class="tabular-nums">0</span>
|
Cash the board · <span data-cash-amount class="tabular-nums">0</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -111,33 +111,33 @@
|
|||||||
<div data-won-controls class="hidden">
|
<div data-won-controls class="hidden">
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<button type="button" data-finish
|
<button type="button" data-finish
|
||||||
class="pete-finish rounded-full bg-[color:var(--accent)] px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
class="pete-finish rounded-full bg-accent px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:pointer-events-none transition">
|
||||||
You've cracked it. Send them all home 🎉
|
You've cracked it. Send them all home 🎉
|
||||||
</button>
|
</button>
|
||||||
<p class="text-xs text-[color:var(--ink)]/45">
|
<p class="text-xs text-muted">
|
||||||
The whole board's face up now, so the rest plays itself.
|
The whole board's face up now, so the rest plays itself.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p data-game-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
<p data-game-msg class="hidden mt-3 rounded-2xl bg-ink/5 px-4 py-2 text-sm font-semibold"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Buying a deck: shown between games. -->
|
<!-- Buying a deck: shown between games. -->
|
||||||
<section data-betting class="rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section data-betting class="rounded-3xl bg-card p-5 sm:p-6 shadow-pete border-2 border-ink/10">
|
||||||
|
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">Which deal?</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">Which deal?</div>
|
||||||
<div class="mt-2 grid gap-2 sm:grid-cols-3">
|
<div class="mt-2 grid gap-2 sm:grid-cols-3">
|
||||||
{{range .Deals}}
|
{{range .Deals}}
|
||||||
<button type="button" data-tier="{{.Slug}}"
|
<button type="button" data-tier="{{.Slug}}"
|
||||||
class="pete-tier rounded-2xl border-2 p-3 text-left transition">
|
class="pete-tier rounded-2xl border-2 p-3 text-left transition">
|
||||||
<div class="flex items-baseline justify-between gap-2">
|
<div class="flex items-baseline justify-between gap-2">
|
||||||
<span class="font-display text-lg font-bold">{{.Name}}</span>
|
<span class="font-display text-lg font-bold">{{.Name}}</span>
|
||||||
<span class="font-display text-lg font-bold tabular-nums text-[color:var(--accent)]">{{printf "%.1f" .Base}}×</span>
|
<span class="font-display text-lg font-bold tabular-nums text-accent">{{printf "%.1f" .Base}}×</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-0.5 text-xs text-[color:var(--ink)]/50">{{.Blurb}}</p>
|
<p class="mt-0.5 text-xs text-muted">{{.Blurb}}</p>
|
||||||
<p class="mt-1.5 text-xs font-semibold text-[color:var(--ink)]/40">
|
<p class="mt-1.5 text-xs font-semibold text-muted">
|
||||||
Square with the house at {{.BreakEven}} cards home.
|
Square with the house at {{.BreakEven}} cards home.
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
|
|
||||||
<div class="mt-5 flex flex-wrap items-center gap-x-6 gap-y-4">
|
<div class="mt-5 flex flex-wrap items-center gap-x-6 gap-y-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">The deck costs</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">The deck costs</div>
|
||||||
<div class="font-display text-3xl font-bold tabular-nums"><span data-bet-amount>0</span></div>
|
<div class="font-display text-3xl font-bold tabular-nums"><span data-bet-amount>0</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -158,29 +158,29 @@
|
|||||||
</button>
|
</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
<button type="button" data-bet-clear
|
<button type="button" data-bet-clear
|
||||||
class="rounded-full px-3 py-2 text-sm font-semibold text-[color:var(--ink)]/50 hover:text-[color:var(--ink)] transition">Clear</button>
|
class="rounded-full px-3 py-2 text-sm font-semibold text-muted hover:text-ink transition">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" data-start
|
<button type="button" data-start
|
||||||
class="ml-auto rounded-full bg-[color:var(--accent)] px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
class="ml-auto rounded-full bg-accent px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Buy the deck
|
Buy the deck
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 text-xs text-[color:var(--ink)]/40">
|
<p class="mt-3 text-xs text-muted">
|
||||||
The stake buys the deck outright, so it doesn't come back. What comes back is
|
The stake buys the deck outright, so it doesn't come back. What comes back is
|
||||||
whatever you get home, a fifty-second of the multiple at a time. Stop whenever
|
whatever you get home, a fifty-second of the multiple at a time. Stop whenever
|
||||||
you like and keep it. There's no undo.
|
you like and keep it. There's no undo.
|
||||||
</p>
|
</p>
|
||||||
<p data-table-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
<p data-table-msg class="hidden mt-3 rounded-2xl bg-ink/5 px-4 py-2 text-sm font-semibold"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "scripts"}}
|
{{define "scripts"}}
|
||||||
<script src="/static/js/casino-fx.js" defer></script>
|
<script src="{{asset "/static/js/casino-fx.js"}}" defer></script>
|
||||||
<script src="/static/js/casino-cards.js" defer></script>
|
<script src="{{asset "/static/js/casino-cards.js"}}" defer></script>
|
||||||
<script src="/static/js/games.js" defer></script>
|
<script src="{{asset "/static/js/games.js"}}" defer></script>
|
||||||
<script src="/static/js/solitaire.js" defer></script>
|
<script src="{{asset "/static/js/solitaire.js"}}" defer></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -23,11 +23,11 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{{if .Standings.Rows}}
|
{{if .Standings.Rows}}
|
||||||
<div class="mt-6 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 shadow-pete overflow-hidden">
|
<div class="mt-6 rounded-3xl bg-card border-2 border-ink/10 shadow-pete overflow-hidden">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="text-[11px] uppercase tracking-wider text-[color:var(--ink)]/45 border-b-2 border-[color:var(--ink)]/10">
|
<tr class="text-[11px] uppercase tracking-wider text-muted border-b-2 border-ink/10">
|
||||||
<th class="text-left font-semibold px-4 py-3 w-10">#</th>
|
<th class="text-left font-semibold px-4 py-3 w-10">#</th>
|
||||||
<th class="text-left font-semibold px-2 py-3">Adventurer</th>
|
<th class="text-left font-semibold px-2 py-3">Adventurer</th>
|
||||||
<th class="text-right font-semibold px-2 py-3" title="The deepest tier they have actually beaten a boss in">Deepest</th>
|
<th class="text-right font-semibold px-2 py-3" title="The deepest tier they have actually beaten a boss in">Deepest</th>
|
||||||
@@ -40,28 +40,28 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range .Standings.Rows}}
|
{{range .Standings.Rows}}
|
||||||
<tr class="border-b border-[color:var(--ink)]/5 last:border-0 hover:bg-[color:var(--ink)]/[0.03] transition">
|
<tr class="border-b border-ink/5 last:border-0 hover:bg-ink/[0.03] transition">
|
||||||
<td class="px-4 py-3 tabular-nums text-[color:var(--ink)]/35 font-semibold">{{.Rank}}</td>
|
<td class="px-4 py-3 tabular-nums text-muted font-semibold">{{.Rank}}</td>
|
||||||
<td class="px-2 py-3 min-w-0">
|
<td class="px-2 py-3 min-w-0">
|
||||||
{{if .Token}}<a href="/adventure/who/{{.Token}}" class="font-semibold hover:text-theme-adventure transition">{{.Name}}</a>
|
{{if .Token}}<a href="/adventure/who/{{.Token}}" class="font-semibold hover:text-theme-adventure transition">{{.Name}}</a>
|
||||||
{{else}}<span class="font-semibold">{{.Name}}</span>{{end}}
|
{{else}}<span class="font-semibold">{{.Name}}</span>{{end}}
|
||||||
<span class="block text-xs text-[color:var(--ink)]/40">
|
<span class="block text-xs text-muted">
|
||||||
lv {{.Level}}{{if .ClassRace}} · {{.ClassRace}}{{end}}
|
lv {{.Level}}{{if .ClassRace}} · {{.ClassRace}}{{end}}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-3 text-right tabular-nums">
|
<td class="px-2 py-3 text-right tabular-nums">
|
||||||
{{if .DeepestTier}}<span class="standings-tier standings-tier-{{.DeepestTier}}">T{{.DeepestTier}}</span>
|
{{if .DeepestTier}}<span class="standings-tier standings-tier-{{.DeepestTier}}">T{{.DeepestTier}}</span>
|
||||||
{{else}}<span class="text-[color:var(--ink)]/25">—</span>{{end}}
|
{{else}}<span class="text-ink/25">—</span>{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-3 text-right tabular-nums{{if not .Zones}} text-[color:var(--ink)]/25{{end}}">{{if .Zones}}{{.Zones}}{{else}}—{{end}}</td>
|
<td class="px-2 py-3 text-right tabular-nums{{if not .Zones}} text-ink/25{{end}}">{{if .Zones}}{{.Zones}}{{else}}—{{end}}</td>
|
||||||
<td class="px-2 py-3 text-right tabular-nums{{if not .Clears}} text-[color:var(--ink)]/25{{end}}">{{if .Clears}}{{.Clears}}{{else}}—{{end}}</td>
|
<td class="px-2 py-3 text-right tabular-nums{{if not .Clears}} text-ink/25{{end}}">{{if .Clears}}{{.Clears}}{{else}}—{{end}}</td>
|
||||||
<td class="px-2 py-3 text-right tabular-nums">
|
<td class="px-2 py-3 text-right tabular-nums">
|
||||||
{{if .Firsts}}<span class="standings-firsts">{{.Firsts}}</span>{{else}}<span class="text-[color:var(--ink)]/25">—</span>{{end}}
|
{{if .Firsts}}<span class="standings-firsts">{{.Firsts}}</span>{{else}}<span class="text-ink/25">—</span>{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 py-3 text-right tabular-nums{{if not .SiegeDamage}} text-[color:var(--ink)]/25{{end}}">
|
<td class="px-2 py-3 text-right tabular-nums{{if not .SiegeDamage}} text-ink/25{{end}}">
|
||||||
{{if .SiegeDamage}}{{.SiegeDamage}}{{else}}—{{end}}
|
{{if .SiegeDamage}}{{.SiegeDamage}}{{else}}—{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-right tabular-nums{{if not .Deaths}} text-[color:var(--ink)]/25{{end}}">{{if .Deaths}}{{.Deaths}}{{else}}—{{end}}</td>
|
<td class="px-4 py-3 text-right tabular-nums{{if not .Deaths}} text-ink/25{{end}}">{{if .Deaths}}{{.Deaths}}{{else}}—{{end}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -69,14 +69,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mt-3 text-xs text-[color:var(--ink)]/40 px-1">
|
<p class="mt-3 text-xs text-muted px-1">
|
||||||
Deepest is the hardest tier they've actually put a boss down in, not the hardest
|
Deepest is the hardest tier they've actually put a boss down in, not the hardest
|
||||||
one they've walked into. Deaths is my own count, off the dispatches I've filed.
|
one they've walked into. Deaths is my own count, off the dispatches I've filed.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="mt-6 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
<div class="mt-6 rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete">
|
||||||
<p class="text-sm text-[color:var(--ink)]/60">
|
<p class="text-sm text-muted">
|
||||||
{{if .Standings.Known}}
|
{{if .Standings.Known}}
|
||||||
Nobody on the board yet. Make a character and it fills up.
|
Nobody on the board yet. Make a character and it fills up.
|
||||||
{{else}}
|
{{else}}
|
||||||
@@ -89,16 +89,16 @@
|
|||||||
{{/* Pete keeps score on himself. He can be hired onto an expedition and he
|
{{/* Pete keeps score on himself. He can be hired onto an expedition and he
|
||||||
duels; a paper that ranks everybody else and quietly leaves itself off the
|
duels; a paper that ranks everybody else and quietly leaves itself off the
|
||||||
table is doing something slightly dishonest. */}}
|
table is doing something slightly dishonest. */}}
|
||||||
<section class="mt-8 rounded-3xl bg-[color:var(--card)] border-2 border-theme-adventure/30 p-6 shadow-pete">
|
<section class="mt-8 rounded-3xl bg-card border-2 border-theme-adventure/30 p-6 shadow-pete">
|
||||||
<h2 class="font-display text-xl font-bold">And my own record, since you asked</h2>
|
<h2 class="font-display text-xl font-bold">And my own record, since you asked</h2>
|
||||||
{{if .Standings.PeteFought}}
|
{{if .Standings.PeteFought}}
|
||||||
<p class="mt-2 text-sm text-[color:var(--ink)]/70">
|
<p class="mt-2 text-sm text-muted">
|
||||||
<span class="font-semibold text-theme-adventure tabular-nums text-lg">{{.Standings.PeteWins}}</span> won,
|
<span class="font-semibold text-theme-adventure tabular-nums text-lg">{{.Standings.PeteWins}}</span> won,
|
||||||
<span class="font-semibold tabular-nums text-lg">{{.Standings.PeteLosses}}</span> lost.
|
<span class="font-semibold tabular-nums text-lg">{{.Standings.PeteLosses}}</span> lost.
|
||||||
I file those myself, which you're welcome to hold against me.
|
I file those myself, which you're welcome to hold against me.
|
||||||
</p>
|
</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="mt-2 text-sm text-[color:var(--ink)]/60">
|
<p class="mt-2 text-sm text-muted">
|
||||||
No bouts yet. I'll report them when there are, wins and the other kind.
|
No bouts yet. I'll report them when there are, wins and the other kind.
|
||||||
</p>
|
</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
{{$admin := .Admin}}
|
{{$admin := .Admin}}
|
||||||
<section class="mt-2 mb-8">
|
<section class="mt-2 mb-8">
|
||||||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 sm:p-8 shadow-pete">
|
<div class="rounded-3xl bg-card border-2 border-ink/10 p-6 sm:p-8 shadow-pete">
|
||||||
<p class="text-sm uppercase tracking-[0.2em] text-[color:var(--ink)]/50">{{if $admin}}owner view{{else}}live status{{end}}</p>
|
<p class="text-sm uppercase tracking-[0.2em] text-muted">{{if $admin}}owner view{{else}}live status{{end}}</p>
|
||||||
<h1 class="font-display text-3xl sm:text-4xl font-bold mt-1">Source health</h1>
|
<h1 class="font-display text-3xl sm:text-4xl font-bold mt-1">Source health</h1>
|
||||||
<p class="mt-2 max-w-2xl text-[color:var(--ink)]/70">
|
<p class="mt-2 max-w-2xl text-muted">
|
||||||
{{if $admin}}Per-feed poll status and content stats. Rows that are currently failing float to the top.
|
{{if $admin}}Per-feed poll status and content stats. Rows that are currently failing float to the top.
|
||||||
{{else}}Every feed Pete follows and whether it's still updating. This runs on a best-effort basis, so a source going quiet for a bit is normal.{{end}}
|
{{else}}Every feed Pete follows and whether it's still updating. This runs on a best-effort basis, so a source going quiet for a bit is normal.{{end}}
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-[color:var(--ink)]/5 px-3 py-1 text-sm font-semibold tabular-nums">
|
<span class="inline-flex items-center gap-1.5 rounded-full bg-ink/5 px-3 py-1 text-sm font-semibold tabular-nums">
|
||||||
{{len .Sources}} sources
|
{{len .Sources}} sources
|
||||||
</span>
|
</span>
|
||||||
{{if .DegradedCnt}}
|
{{if .DegradedCnt}}
|
||||||
@@ -34,15 +34,15 @@
|
|||||||
<section class="mb-8">
|
<section class="mb-8">
|
||||||
<div class="rounded-3xl bg-amber-500/10 border-2 border-amber-500/30 p-6">
|
<div class="rounded-3xl bg-amber-500/10 border-2 border-amber-500/30 p-6">
|
||||||
<h2 class="font-display text-xl font-bold">Dispatches with no template</h2>
|
<h2 class="font-display text-xl font-bold">Dispatches with no template</h2>
|
||||||
<p class="mt-1 text-sm text-[color:var(--ink)]/70 max-w-2xl">
|
<p class="mt-1 text-sm text-muted max-w-2xl">
|
||||||
gogobee sent these event types since the last restart and Pete published them on the
|
gogobee sent these event types since the last restart and Pete published them on the
|
||||||
generic fallback. Nothing was lost — they just read thin until he learns the words.
|
generic fallback. Nothing was lost — they just read thin until he learns the words.
|
||||||
</p>
|
</p>
|
||||||
<ul class="mt-3 flex flex-wrap gap-2">
|
<ul class="mt-3 flex flex-wrap gap-2">
|
||||||
{{range .UnknownAdv}}
|
{{range .UnknownAdv}}
|
||||||
<li class="inline-flex items-center gap-2 rounded-full bg-[color:var(--card)] border border-amber-500/40 px-3 py-1 text-sm">
|
<li class="inline-flex items-center gap-2 rounded-full bg-card border border-amber-500/40 px-3 py-1 text-sm">
|
||||||
<code class="font-mono">{{.EventType}}</code>
|
<code class="font-mono">{{.EventType}}</code>
|
||||||
<span class="tabular-nums text-[color:var(--ink)]/60">×{{.Count}}</span>
|
<span class="tabular-nums text-muted">×{{.Count}}</span>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -50,10 +50,10 @@
|
|||||||
</section>
|
</section>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="overflow-x-auto rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 shadow-pete">
|
<div class="overflow-x-auto rounded-3xl bg-card border-2 border-ink/10 shadow-pete">
|
||||||
<table class="w-full {{if $admin}}min-w-[52rem]{{else}}min-w-[28rem]{{end}} text-sm">
|
<table class="w-full {{if $admin}}min-w-[52rem]{{else}}min-w-[28rem]{{end}} text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="text-left text-[color:var(--ink)]/50 uppercase text-xs tracking-wider border-b-2 border-[color:var(--ink)]/10">
|
<tr class="text-left text-muted uppercase text-xs tracking-wider border-b-2 border-ink/10">
|
||||||
<th class="px-4 py-3 font-semibold">Source</th>
|
<th class="px-4 py-3 font-semibold">Source</th>
|
||||||
<th class="px-4 py-3 font-semibold">Channel</th>
|
<th class="px-4 py-3 font-semibold">Channel</th>
|
||||||
<th class="px-4 py-3 font-semibold">Status</th>
|
<th class="px-4 py-3 font-semibold">Status</th>
|
||||||
@@ -70,12 +70,12 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range .Sources}}
|
{{range .Sources}}
|
||||||
<tr class="border-b border-[color:var(--ink)]/5 last:border-0 align-top">
|
<tr class="border-b border-ink/5 last:border-0 align-top">
|
||||||
<td class="px-4 py-3 font-semibold whitespace-nowrap">{{.Name}}</td>
|
<td class="px-4 py-3 font-semibold whitespace-nowrap">{{.Name}}</td>
|
||||||
<td class="px-4 py-3 text-[color:var(--ink)]/70 whitespace-nowrap">{{.Channel}}</td>
|
<td class="px-4 py-3 text-muted whitespace-nowrap">{{.Channel}}</td>
|
||||||
<td class="px-4 py-3 whitespace-nowrap">
|
<td class="px-4 py-3 whitespace-nowrap">
|
||||||
{{if .NeverRun}}
|
{{if .NeverRun}}
|
||||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-[color:var(--ink)]/10 px-2.5 py-1 text-xs font-semibold">◌ idle</span>
|
<span class="inline-flex items-center gap-1.5 rounded-full bg-ink/10 px-2.5 py-1 text-xs font-semibold">◌ idle</span>
|
||||||
{{else if .Healthy}}
|
{{else if .Healthy}}
|
||||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 px-2.5 py-1 text-xs font-semibold">● live</span>
|
<span class="inline-flex items-center gap-1.5 rounded-full bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 px-2.5 py-1 text-xs font-semibold">● live</span>
|
||||||
{{else if $admin}}
|
{{else if $admin}}
|
||||||
@@ -85,25 +85,25 @@
|
|||||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-red-500/15 text-red-700 dark:text-red-300 px-2.5 py-1 text-xs font-semibold">✕ delayed</span>
|
<span class="inline-flex items-center gap-1.5 rounded-full bg-red-500/15 text-red-700 dark:text-red-300 px-2.5 py-1 text-xs font-semibold">✕ delayed</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-[color:var(--ink)]/70 whitespace-nowrap">{{if .LastSuccessAt.IsZero}}never{{else}}{{timeAgo .LastSuccessAt}}{{end}}</td>
|
<td class="px-4 py-3 text-muted whitespace-nowrap">{{if .LastSuccessAt.IsZero}}never{{else}}{{timeAgo .LastSuccessAt}}{{end}}</td>
|
||||||
{{if $admin}}
|
{{if $admin}}
|
||||||
<td class="px-4 py-3 text-[color:var(--ink)]/70 whitespace-nowrap">{{if .LastPollAt.IsZero}}never{{else}}{{timeAgo .LastPollAt}}{{end}}</td>
|
<td class="px-4 py-3 text-muted whitespace-nowrap">{{if .LastPollAt.IsZero}}never{{else}}{{timeAgo .LastPollAt}}{{end}}</td>
|
||||||
<td class="px-4 py-3 text-right tabular-nums">{{.LastItemCount}}</td>
|
<td class="px-4 py-3 text-right tabular-nums">{{.LastItemCount}}</td>
|
||||||
<td class="px-4 py-3 text-right tabular-nums" title="{{.Classified}} classified of {{.Total}}">{{.Total}}</td>
|
<td class="px-4 py-3 text-right tabular-nums" title="{{.Classified}} classified of {{.Total}}">{{.Total}}</td>
|
||||||
<td class="px-4 py-3 text-right tabular-nums {{if gt .PaywallRate 50}}text-amber-600 dark:text-amber-400 font-semibold{{end}}">
|
<td class="px-4 py-3 text-right tabular-nums {{if gt .PaywallRate 50}}text-amber-600 dark:text-amber-400 font-semibold{{end}}">
|
||||||
{{if .Total}}{{.PaywallRate}}%{{else}}—{{end}}
|
{{if .Total}}{{.PaywallRate}}%{{else}}—{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-[color:var(--ink)]/70 whitespace-nowrap">{{if .LastSeenAt.IsZero}}—{{else}}{{timeAgo .LastSeenAt}}{{end}}</td>
|
<td class="px-4 py-3 text-muted whitespace-nowrap">{{if .LastSeenAt.IsZero}}—{{else}}{{timeAgo .LastSeenAt}}{{end}}</td>
|
||||||
<td class="px-4 py-3 text-[color:var(--ink)]/70 whitespace-nowrap">{{if .LastPostedAt.IsZero}}—{{else}}{{timeAgo .LastPostedAt}}{{end}}</td>
|
<td class="px-4 py-3 text-muted whitespace-nowrap">{{if .LastPostedAt.IsZero}}—{{else}}{{timeAgo .LastPostedAt}}{{end}}</td>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tr>
|
</tr>
|
||||||
{{if $admin}}{{if .LastError}}{{if not .Healthy}}
|
{{if $admin}}{{if .LastError}}{{if not .Healthy}}
|
||||||
<tr class="border-b border-[color:var(--ink)]/5">
|
<tr class="border-b border-ink/5">
|
||||||
<td colspan="10" class="px-4 pb-3 -mt-1 text-xs text-red-600 dark:text-red-400 font-mono break-all">{{.LastError}}</td>
|
<td colspan="10" class="px-4 pb-3 -mt-1 text-xs text-red-600 dark:text-red-400 font-mono break-all">{{.LastError}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}{{end}}{{end}}
|
{{end}}{{end}}{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<tr><td colspan="{{if $admin}}10{{else}}4{{end}}" class="px-4 py-8 text-center text-[color:var(--ink)]/50">No sources configured.</td></tr>
|
<tr><td colspan="{{if $admin}}10{{else}}4{{end}}" class="px-4 py-8 text-center text-muted">No sources configured.</td></tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
<article class="mt-2 mb-10 max-w-3xl mx-auto">
|
<article class="mt-2 mb-10 max-w-3xl mx-auto">
|
||||||
<nav class="mb-4">
|
<nav class="mb-4">
|
||||||
<a href="/adventure" class="inline-flex items-center gap-1.5 text-sm font-semibold text-[color:var(--ink)]/60 hover:text-[color:var(--ink)] transition">
|
<a href="/adventure" class="inline-flex items-center gap-1.5 text-sm font-semibold text-muted hover:text-ink transition">
|
||||||
<span aria-hidden="true">←</span> All dispatches
|
<span aria-hidden="true">←</span> All dispatches
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="mt-8 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 sm:p-8 shadow-pete">
|
<div class="mt-8 rounded-3xl bg-card border-2 border-ink/10 p-6 sm:p-8 shadow-pete">
|
||||||
<p class="text-lg leading-relaxed text-[color:var(--ink)]/90 whitespace-pre-line">{{.Body}}</p>
|
<p class="text-lg leading-relaxed text-ink/90 whitespace-pre-line">{{.Body}}</p>
|
||||||
{{if .RunReportURL}}
|
{{if .RunReportURL}}
|
||||||
<!-- The way back to what actually happened. This dispatch is the ending of an
|
<!-- The way back to what actually happened. This dispatch is the ending of an
|
||||||
expedition; the report is the expedition. -->
|
expedition; the report is the expedition. -->
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<span aria-hidden="true">📜</span> Read the run, room by room <span aria-hidden="true">→</span>
|
<span aria-hidden="true">📜</span> Read the run, room by room <span aria-hidden="true">→</span>
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
<p class="mt-8 pt-6 border-t border-[color:var(--ink)]/10 text-sm italic text-theme-adventure font-semibold">
|
<p class="mt-8 pt-6 border-t border-ink/10 text-sm italic text-theme-adventure font-semibold">
|
||||||
Reporting from the realm, this is Pete.
|
Reporting from the realm, this is Pete.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div class="flex items-center gap-3 min-w-0">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
<a href="/games" class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-[color:var(--card)] shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition" title="Back to the casino">
|
<a href="/games" class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-card shadow-pete border-2 border-ink/10 hover:bg-ink/5 transition" title="Back to the casino">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5" aria-hidden="true">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5" aria-hidden="true">
|
||||||
<path d="M19 12H5"></path><polyline points="12 19 5 12 12 5"></polyline>
|
<path d="M19 12H5"></path><polyline points="12 19 5 12 12 5"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -13,14 +13,14 @@
|
|||||||
</a>
|
</a>
|
||||||
<h1 class="font-display text-3xl font-bold">Trivia</h1>
|
<h1 class="font-display text-3xl font-bold">Trivia</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-[color:var(--ink)]/50">{{.Rungs}} questions · answer fast, or don't bother</p>
|
<p class="text-sm text-muted">{{.Rungs}} questions · answer fast, or don't bother</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{template "_chipbar" .}}
|
{{template "_chipbar" .}}
|
||||||
|
|
||||||
<!-- The felt. The clock is the biggest thing on it, because the clock is the
|
<!-- The felt. The clock is the biggest thing on it, because the clock is the
|
||||||
game: a right answer is worth what it's worth *when you give it*. -->
|
game: a right answer is worth what it's worth *when you give it*. -->
|
||||||
<section class="pete-felt relative overflow-hidden rounded-3xl p-6 sm:p-10 shadow-pete-lg border-2 border-[color:var(--ink)]/10">
|
<section class="pete-felt relative overflow-hidden rounded-3xl p-6 sm:p-10 shadow-pete-lg border-2 border-ink/10">
|
||||||
|
|
||||||
<div class="pete-rack" data-house aria-hidden="true">
|
<div class="pete-rack" data-house aria-hidden="true">
|
||||||
<span data-chip="500" style="--stack: 5"></span>
|
<span data-chip="500" style="--stack: 5"></span>
|
||||||
@@ -76,34 +76,34 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Playing: shown while a ladder is live. -->
|
<!-- Playing: shown while a ladder is live. -->
|
||||||
<section data-playing class="hidden rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section data-playing class="hidden rounded-3xl bg-card p-5 sm:p-6 shadow-pete border-2 border-ink/10">
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<p class="text-sm text-[color:var(--ink)]/50">
|
<p class="text-sm text-muted">
|
||||||
Press <span class="font-bold">1</span>–<span class="font-bold">4</span>, or click one. A wrong answer, or the clock, loses the lot.
|
Press <span class="font-bold">1</span>–<span class="font-bold">4</span>, or click one. A wrong answer, or the clock, loses the lot.
|
||||||
</p>
|
</p>
|
||||||
<button type="button" data-walk
|
<button type="button" data-walk
|
||||||
class="ml-auto rounded-full bg-[color:var(--accent)] px-6 py-3 font-display text-lg font-bold text-white shadow-pete
|
class="ml-auto rounded-full bg-accent px-6 py-3 font-display text-lg font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Take the money · <span data-walk-amount>0</span>
|
Take the money · <span data-walk-amount>0</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p data-game-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
<p data-game-msg class="hidden mt-3 rounded-2xl bg-ink/5 px-4 py-2 text-sm font-semibold"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Betting: shown between games. -->
|
<!-- Betting: shown between games. -->
|
||||||
<section data-betting class="rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section data-betting class="rounded-3xl bg-card p-5 sm:p-6 shadow-pete border-2 border-ink/10">
|
||||||
|
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">How hard?</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">How hard?</div>
|
||||||
<div class="mt-2 grid gap-2 sm:grid-cols-3">
|
<div class="mt-2 grid gap-2 sm:grid-cols-3">
|
||||||
{{range .Quizzes}}
|
{{range .Quizzes}}
|
||||||
<button type="button" data-tier="{{.Slug}}"
|
<button type="button" data-tier="{{.Slug}}"
|
||||||
class="pete-tier rounded-2xl border-2 p-3 text-left transition">
|
class="pete-tier rounded-2xl border-2 p-3 text-left transition">
|
||||||
<div class="flex items-baseline justify-between gap-2">
|
<div class="flex items-baseline justify-between gap-2">
|
||||||
<span class="font-display text-lg font-bold">{{.Name}}</span>
|
<span class="font-display text-lg font-bold">{{.Name}}</span>
|
||||||
<span class="font-display text-lg font-bold tabular-nums text-[color:var(--accent)]">{{printf "%.2f" .Fast}}×</span>
|
<span class="font-display text-lg font-bold tabular-nums text-accent">{{printf "%.2f" .Fast}}×</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-0.5 text-xs text-[color:var(--ink)]/50">{{.Blurb}}</p>
|
<p class="mt-0.5 text-xs text-muted">{{.Blurb}}</p>
|
||||||
<p class="mt-1.5 text-xs font-semibold text-[color:var(--ink)]/40">
|
<p class="mt-1.5 text-xs font-semibold text-muted">
|
||||||
{{.Limit}}s a question · slowest answer still pays {{printf "%.2f" .Buzzer}}×
|
{{.Limit}}s a question · slowest answer still pays {{printf "%.2f" .Buzzer}}×
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
|
|
||||||
<div class="mt-5 flex flex-wrap items-center gap-x-6 gap-y-4">
|
<div class="mt-5 flex flex-wrap items-center gap-x-6 gap-y-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">Your bet</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">Your bet</div>
|
||||||
<div class="font-display text-3xl font-bold tabular-nums"><span data-bet-amount>0</span></div>
|
<div class="font-display text-3xl font-bold tabular-nums"><span data-bet-amount>0</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -124,26 +124,26 @@
|
|||||||
</button>
|
</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
<button type="button" data-bet-clear
|
<button type="button" data-bet-clear
|
||||||
class="rounded-full px-3 py-2 text-sm font-semibold text-[color:var(--ink)]/50 hover:text-[color:var(--ink)] transition">Clear</button>
|
class="rounded-full px-3 py-2 text-sm font-semibold text-muted hover:text-ink transition">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" data-start
|
<button type="button" data-start
|
||||||
class="ml-auto rounded-full bg-[color:var(--accent)] px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
class="ml-auto rounded-full bg-accent px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Play
|
Play
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-3 text-center text-xs text-[color:var(--ink)]/40">
|
<p class="mt-3 text-center text-xs text-muted">
|
||||||
The first question is the price of sitting down: you can only walk once you've answered one.
|
The first question is the price of sitting down: you can only walk once you've answered one.
|
||||||
</p>
|
</p>
|
||||||
<p data-table-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
<p data-table-msg class="hidden mt-3 rounded-2xl bg-ink/5 px-4 py-2 text-sm font-semibold"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "scripts"}}
|
{{define "scripts"}}
|
||||||
<script src="/static/js/casino-fx.js" defer></script>
|
<script src="{{asset "/static/js/casino-fx.js"}}" defer></script>
|
||||||
<script src="/static/js/games.js" defer></script>
|
<script src="{{asset "/static/js/games.js"}}" defer></script>
|
||||||
<script src="/static/js/trivia.js" defer></script>
|
<script src="{{asset "/static/js/trivia.js"}}" defer></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div class="flex items-center gap-3 min-w-0">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
<a href="/games" class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-[color:var(--card)] shadow-pete border-2 border-[color:var(--ink)]/10 hover:bg-[color:var(--ink)]/5 transition" title="Back to the casino">
|
<a href="/games" class="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-card shadow-pete border-2 border-ink/10 hover:bg-ink/5 transition" title="Back to the casino">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5" aria-hidden="true">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5" aria-hidden="true">
|
||||||
<path d="M19 12H5"></path><polyline points="12 19 5 12 12 5"></polyline>
|
<path d="M19 12H5"></path><polyline points="12 19 5 12 12 5"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<h1 class="font-display text-3xl font-bold">UNO</h1>
|
<h1 class="font-display text-3xl font-bold">UNO</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-[color:var(--ink)]/50">Sit down, ante up, and go out first — the pot's yours</p>
|
<p class="text-sm text-muted">Sit down, ante up, and go out first — the pot's yours</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{template "_chipbar" .}}
|
{{template "_chipbar" .}}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<!-- The felt. Other seats along the top, the deck and the pile in the middle
|
<!-- The felt. Other seats along the top, the deck and the pile in the middle
|
||||||
with the pot beside them, your hand at the bottom. Every ante on this table
|
with the pot beside them, your hand at the bottom. Every ante on this table
|
||||||
is riding in the pot until somebody goes out and takes it. -->
|
is riding in the pot until somebody goes out and takes it. -->
|
||||||
<section class="pete-felt pete-uno relative overflow-hidden rounded-3xl p-4 sm:p-6 lg:p-8 shadow-pete-lg border-2 border-[color:var(--ink)]/10">
|
<section class="pete-felt pete-uno relative overflow-hidden rounded-3xl p-4 sm:p-6 lg:p-8 shadow-pete-lg border-2 border-ink/10">
|
||||||
<div class="grid gap-5 lg:grid-cols-[minmax(0,1fr),auto] lg:gap-8">
|
<div class="grid gap-5 lg:grid-cols-[minmax(0,1fr),auto] lg:gap-8">
|
||||||
|
|
||||||
<div class="min-w-0 space-y-5">
|
<div class="min-w-0 space-y-5">
|
||||||
@@ -109,15 +109,15 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Acting: shown while it's your turn. -->
|
<!-- Acting: shown while it's your turn. -->
|
||||||
<section data-acting class="hidden rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section data-acting class="hidden rounded-3xl bg-card p-5 sm:p-6 shadow-pete border-2 border-ink/10">
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<p data-hint class="text-sm text-[color:var(--ink)]/50">
|
<p data-hint class="text-sm text-muted">
|
||||||
Click a card that lights up. Nothing lights up? Draw one.
|
Click a card that lights up. Nothing lights up? Draw one.
|
||||||
</p>
|
</p>
|
||||||
<div class="ml-auto flex flex-wrap items-center gap-2">
|
<div class="ml-auto flex flex-wrap items-center gap-2">
|
||||||
<button type="button" data-draw
|
<button type="button" data-draw
|
||||||
class="rounded-full border-2 border-[color:var(--ink)]/15 bg-[color:var(--card)] px-6 py-3 font-display text-lg font-bold shadow-pete
|
class="rounded-full border-2 border-ink/15 bg-card px-6 py-3 font-display text-lg font-bold shadow-pete
|
||||||
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:bg-ink/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Draw
|
Draw
|
||||||
</button>
|
</button>
|
||||||
<button type="button" data-take
|
<button type="button" data-take
|
||||||
@@ -126,60 +126,60 @@
|
|||||||
Take <span data-take-n>0</span>
|
Take <span data-take-n>0</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" data-pass
|
<button type="button" data-pass
|
||||||
class="hidden rounded-full bg-[color:var(--accent)] px-6 py-3 font-display text-lg font-bold text-white shadow-pete
|
class="hidden rounded-full bg-accent px-6 py-3 font-display text-lg font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Keep it
|
Keep it
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p data-game-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
<p data-game-msg class="hidden mt-3 rounded-2xl bg-ink/5 px-4 py-2 text-sm font-semibold"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Between hands: deal the next one, or get up. -->
|
<!-- Between hands: deal the next one, or get up. -->
|
||||||
<section data-between class="hidden rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section data-between class="hidden rounded-3xl bg-card p-5 sm:p-6 shadow-pete border-2 border-ink/10">
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">In front of you</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">In front of you</div>
|
||||||
<div class="font-display text-3xl font-bold tabular-nums" data-table-stack>0</div>
|
<div class="font-display text-3xl font-bold tabular-nums" data-table-stack>0</div>
|
||||||
<p class="mt-0.5 text-xs text-[color:var(--ink)]/40">
|
<p class="mt-0.5 text-xs text-muted">
|
||||||
Bought in for <span data-bought-in class="tabular-nums">0</span> · <span data-ante-note class="tabular-nums">0</span> to ante
|
Bought in for <span data-bought-in class="tabular-nums">0</span> · <span data-ante-note class="tabular-nums">0</span> to ante
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-auto flex flex-wrap items-center gap-2">
|
<div class="ml-auto flex flex-wrap items-center gap-2">
|
||||||
<button type="button" data-leave
|
<button type="button" data-leave
|
||||||
class="rounded-full border-2 border-[color:var(--ink)]/15 bg-[color:var(--card)] px-5 py-3 font-display text-base font-bold shadow-pete
|
class="rounded-full border-2 border-ink/15 bg-card px-5 py-3 font-display text-base font-bold shadow-pete
|
||||||
hover:bg-[color:var(--ink)]/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:bg-ink/5 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Get up
|
Get up
|
||||||
</button>
|
</button>
|
||||||
<button type="button" data-deal
|
<button type="button" data-deal
|
||||||
class="rounded-full bg-[color:var(--accent)] px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
class="rounded-full bg-accent px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Next hand
|
Next hand
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p data-between-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
<p data-between-msg class="hidden mt-3 rounded-2xl bg-ink/5 px-4 py-2 text-sm font-semibold"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- The rail talk, shown while you're seated. -->
|
<!-- The rail talk, shown while you're seated. -->
|
||||||
<section data-chat class="hidden rounded-3xl bg-[color:var(--card)] p-4 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section data-chat class="hidden rounded-3xl bg-card p-4 shadow-pete border-2 border-ink/10">
|
||||||
<div data-chat-log class="flex flex-col gap-1 max-h-40 overflow-y-auto pr-1 text-sm" aria-live="polite" aria-label="Table chat"></div>
|
<div data-chat-log class="flex flex-col gap-1 max-h-40 overflow-y-auto pr-1 text-sm" aria-live="polite" aria-label="Table chat"></div>
|
||||||
<form data-chat-form class="mt-3 flex items-center gap-2">
|
<form data-chat-form class="mt-3 flex items-center gap-2">
|
||||||
<input data-chat-input type="text" maxlength="240" autocomplete="off" placeholder="Say something to the table…"
|
<input data-chat-input type="text" maxlength="240" autocomplete="off" placeholder="Say something to the table…"
|
||||||
class="flex-1 min-w-0 rounded-full border-2 border-[color:var(--ink)]/15 bg-[color:var(--bg)] px-4 py-2 text-sm focus:outline-none focus:border-[color:var(--accent)]">
|
class="flex-1 min-w-0 rounded-full border-2 border-ink/15 bg-[color:var(--bg)] px-4 py-2 text-sm focus:outline-none focus:border-accent">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="rounded-full bg-[color:var(--accent)] px-5 py-2 font-display text-sm font-bold text-white shadow-pete hover:brightness-105 active:translate-y-px transition">
|
class="rounded-full bg-accent px-5 py-2 font-display text-sm font-bold text-white shadow-pete hover:brightness-105 active:translate-y-px transition">
|
||||||
Say
|
Say
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Sitting down: shown when you aren't at a table. -->
|
<!-- Sitting down: shown when you aren't at a table. -->
|
||||||
<section data-sitting class="rounded-3xl bg-[color:var(--card)] p-5 sm:p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section data-sitting class="rounded-3xl bg-card p-5 sm:p-6 shadow-pete border-2 border-ink/10">
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">Which table?</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">Which table?</div>
|
||||||
<div class="pete-seg" role="group" aria-label="Which rules">
|
<div class="pete-seg" role="group" aria-label="Which rules">
|
||||||
<button type="button" data-rules="normal" class="pete-seg-btn">UNO</button>
|
<button type="button" data-rules="normal" class="pete-seg-btn">UNO</button>
|
||||||
<button type="button" data-rules="nomercy" class="pete-seg-btn">No Mercy</button>
|
<button type="button" data-rules="nomercy" class="pete-seg-btn">No Mercy</button>
|
||||||
@@ -193,10 +193,10 @@
|
|||||||
class="pete-tier rounded-2xl border-2 p-3 text-left transition">
|
class="pete-tier rounded-2xl border-2 p-3 text-left transition">
|
||||||
<div class="flex items-baseline justify-between gap-2">
|
<div class="flex items-baseline justify-between gap-2">
|
||||||
<span class="font-display text-lg font-bold">{{.Name}}</span>
|
<span class="font-display text-lg font-bold">{{.Name}}</span>
|
||||||
<span class="font-display text-lg font-bold tabular-nums text-[color:var(--accent)]">{{.Ante}} ante</span>
|
<span class="font-display text-lg font-bold tabular-nums text-accent">{{.Ante}} ante</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-0.5 text-xs text-[color:var(--ink)]/50">{{.Blurb}}</p>
|
<p class="mt-0.5 text-xs text-muted">{{.Blurb}}</p>
|
||||||
<p class="mt-1.5 text-xs font-semibold text-[color:var(--ink)]/40">
|
<p class="mt-1.5 text-xs font-semibold text-muted">
|
||||||
{{.Bots}} bot{{if gt .Bots 1}}s{{end}} · buy in {{.MinBuy}}–{{.MaxBuy}}
|
{{.Bots}} bot{{if gt .Bots 1}}s{{end}} · buy in {{.MinBuy}}–{{.MaxBuy}}
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
@@ -211,10 +211,10 @@
|
|||||||
class="pete-tier pete-tier-nm rounded-2xl border-2 p-3 text-left transition">
|
class="pete-tier pete-tier-nm rounded-2xl border-2 p-3 text-left transition">
|
||||||
<div class="flex items-baseline justify-between gap-2">
|
<div class="flex items-baseline justify-between gap-2">
|
||||||
<span class="font-display text-lg font-bold">{{.Name}}</span>
|
<span class="font-display text-lg font-bold">{{.Name}}</span>
|
||||||
<span class="font-display text-lg font-bold tabular-nums text-[color:var(--accent)]">{{.Ante}} ante</span>
|
<span class="font-display text-lg font-bold tabular-nums text-accent">{{.Ante}} ante</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-0.5 text-xs text-[color:var(--ink)]/50">{{.Blurb}}</p>
|
<p class="mt-0.5 text-xs text-muted">{{.Blurb}}</p>
|
||||||
<p class="mt-1.5 text-xs font-semibold text-[color:var(--ink)]/40">
|
<p class="mt-1.5 text-xs font-semibold text-muted">
|
||||||
{{.Bots}} bot{{if gt .Bots 1}}s{{end}} · 168 cards
|
{{.Bots}} bot{{if gt .Bots 1}}s{{end}} · 168 cards
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
@@ -223,39 +223,39 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5">
|
<div class="mt-5">
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">Buying in for</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">Buying in for</div>
|
||||||
<div class="mt-2 flex items-center gap-3">
|
<div class="mt-2 flex items-center gap-3">
|
||||||
<input type="range" data-buyin-slider class="flex-1 min-w-0" aria-label="How much to buy in for">
|
<input type="range" data-buyin-slider class="flex-1 min-w-0" aria-label="How much to buy in for">
|
||||||
<span data-buyin class="font-display text-2xl font-bold tabular-nums">0</span>
|
<span data-buyin class="font-display text-2xl font-bold tabular-nums">0</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1.5 text-xs text-[color:var(--ink)]/40" data-buyin-note></p>
|
<p class="mt-1.5 text-xs text-muted" data-buyin-note></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" data-sit
|
<button type="button" data-sit
|
||||||
class="mt-5 w-full rounded-full bg-[color:var(--accent)] px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
class="mt-5 w-full rounded-full bg-accent px-8 py-3 font-display text-lg font-bold text-white shadow-pete
|
||||||
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
hover:brightness-105 active:translate-y-px disabled:opacity-40 disabled:pointer-events-none transition">
|
||||||
Sit down
|
Sit down
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div data-lobby-wrap class="mt-6 hidden">
|
<div data-lobby-wrap class="mt-6 hidden">
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-[color:var(--ink)]/50">Or join a table in progress</div>
|
<div class="text-xs font-semibold uppercase tracking-wider text-muted">Or join a table in progress</div>
|
||||||
<div data-lobby class="mt-2 grid gap-2"></div>
|
<div data-lobby class="mt-2 grid gap-2"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p data-note="normal" class="mt-3 text-center text-xs text-[color:var(--ink)]/40">
|
<p data-note="normal" class="mt-3 text-center text-xs text-muted">
|
||||||
Everyone antes, and the first one out takes the pot less {{.RakePct}}% rake. Normal rules: no stacking a +2 on a +2, and a reverse is a skip when it's just the two of you.
|
Everyone antes, and the first one out takes the pot less {{.RakePct}}% rake. Normal rules: no stacking a +2 on a +2, and a reverse is a skip when it's just the two of you.
|
||||||
</p>
|
</p>
|
||||||
<p data-note="nomercy" class="hidden mt-3 text-center text-xs text-[color:var(--ink)]/40">
|
<p data-note="nomercy" class="hidden mt-3 text-center text-xs text-muted">
|
||||||
168 cards, +6s and +10s. Draw cards stack, you draw until you can play, and {{.MercyLimit}} cards in your hand puts you out of the hand. The pot still goes to whoever goes out first.
|
168 cards, +6s and +10s. Draw cards stack, you draw until you can play, and {{.MercyLimit}} cards in your hand puts you out of the hand. The pot still goes to whoever goes out first.
|
||||||
</p>
|
</p>
|
||||||
<p data-table-msg class="hidden mt-3 rounded-2xl bg-[color:var(--ink)]/5 px-4 py-2 text-sm font-semibold"></p>
|
<p data-table-msg class="hidden mt-3 rounded-2xl bg-ink/5 px-4 py-2 text-sm font-semibold"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "scripts"}}
|
{{define "scripts"}}
|
||||||
<script src="/static/js/casino-fx.js" defer></script>
|
<script src="{{asset "/static/js/casino-fx.js"}}" defer></script>
|
||||||
<script src="/static/js/games.js" defer></script>
|
<script src="{{asset "/static/js/games.js"}}" defer></script>
|
||||||
<script src="/static/js/uno.js" defer></script>
|
<script src="{{asset "/static/js/uno.js"}}" defer></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -2,65 +2,65 @@
|
|||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<section class="rounded-3xl bg-[color:var(--card)] p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<section class="rounded-3xl bg-card p-6 shadow-pete border-2 border-ink/10">
|
||||||
<div class="flex flex-wrap items-start justify-between gap-3 mb-1">
|
<div class="flex flex-wrap items-start justify-between gap-3 mb-1">
|
||||||
<h1 class="font-display text-3xl font-bold">Weather demo</h1>
|
<h1 class="font-display text-3xl font-bold">Weather demo</h1>
|
||||||
<div class="flex items-center gap-2 text-xs font-semibold">
|
<div class="flex items-center gap-2 text-xs font-semibold">
|
||||||
<span data-demo-renderer class="rounded-full border-2 border-[color:var(--ink)]/10 px-3 py-1 uppercase tracking-wider text-[color:var(--ink)]/60">renderer: …</span>
|
<span data-demo-renderer class="rounded-full border-2 border-ink/10 px-3 py-1 uppercase tracking-wider text-muted">renderer: …</span>
|
||||||
<span data-demo-fps class="rounded-full border-2 border-[color:var(--ink)]/10 px-3 py-1 tabular-nums text-[color:var(--ink)]/60">— fps</span>
|
<span data-demo-fps class="rounded-full border-2 border-ink/10 px-3 py-1 tabular-nums text-muted">— fps</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-[color:var(--ink)]/60 mb-5">Pick a variant, intensity, and phase. Switching is instant, no reload.</p>
|
<p class="text-sm text-muted mb-5">Pick a variant, intensity, and phase. Switching is instant, no reload.</p>
|
||||||
|
|
||||||
<div class="space-y-3 text-sm">
|
<div class="space-y-3 text-sm">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<span class="w-20 shrink-0 font-semibold uppercase tracking-wider text-[color:var(--ink)]/60">Variant</span>
|
<span class="w-20 shrink-0 font-semibold uppercase tracking-wider text-muted">Variant</span>
|
||||||
{{range $v := .Variants}}
|
{{range $v := .Variants}}
|
||||||
<a href="?variant={{$v}}&intensity={{$.Intensity}}&phase={{$.PhaseSel}}"
|
<a href="?variant={{$v}}&intensity={{$.Intensity}}&phase={{$.PhaseSel}}"
|
||||||
data-demo-variant="{{$v}}"
|
data-demo-variant="{{$v}}"
|
||||||
class="rounded-full px-3 py-1 transition border-2 border-[color:var(--ink)]/10
|
class="rounded-full px-3 py-1 transition border-2 border-ink/10
|
||||||
{{if eq $.Variant $v}}bg-[color:var(--accent)] text-white font-semibold{{else}}hover:bg-[color:var(--ink)]/5{{end}}">{{$v}}</a>
|
{{if eq $.Variant $v}}bg-accent text-white font-semibold{{else}}hover:bg-ink/5{{end}}">{{$v}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<span class="w-20 shrink-0 font-semibold uppercase tracking-wider text-[color:var(--ink)]/60">Intensity</span>
|
<span class="w-20 shrink-0 font-semibold uppercase tracking-wider text-muted">Intensity</span>
|
||||||
{{range $i := .Intensities}}
|
{{range $i := .Intensities}}
|
||||||
<a href="?variant={{$.Variant}}&intensity={{$i}}&phase={{$.PhaseSel}}"
|
<a href="?variant={{$.Variant}}&intensity={{$i}}&phase={{$.PhaseSel}}"
|
||||||
data-demo-intensity="{{$i}}"
|
data-demo-intensity="{{$i}}"
|
||||||
class="rounded-full px-3 py-1 transition border-2 border-[color:var(--ink)]/10
|
class="rounded-full px-3 py-1 transition border-2 border-ink/10
|
||||||
{{if eq $.Intensity $i}}bg-[color:var(--accent)] text-white font-semibold{{else}}hover:bg-[color:var(--ink)]/5{{end}}">{{$i}}</a>
|
{{if eq $.Intensity $i}}bg-accent text-white font-semibold{{else}}hover:bg-ink/5{{end}}">{{$i}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<span class="w-20 shrink-0 font-semibold uppercase tracking-wider text-[color:var(--ink)]/60">Phase</span>
|
<span class="w-20 shrink-0 font-semibold uppercase tracking-wider text-muted">Phase</span>
|
||||||
{{range $p := .Phases}}
|
{{range $p := .Phases}}
|
||||||
<a href="?variant={{$.Variant}}&intensity={{$.Intensity}}&phase={{$p}}"
|
<a href="?variant={{$.Variant}}&intensity={{$.Intensity}}&phase={{$p}}"
|
||||||
data-demo-phase="{{$p}}"
|
data-demo-phase="{{$p}}"
|
||||||
class="rounded-full px-3 py-1 transition border-2 border-[color:var(--ink)]/10
|
class="rounded-full px-3 py-1 transition border-2 border-ink/10
|
||||||
{{if eq $.PhaseSel $p}}bg-[color:var(--accent)] text-white font-semibold{{else}}hover:bg-[color:var(--ink)]/5{{end}}">{{$p}}</a>
|
{{if eq $.PhaseSel $p}}bg-accent text-white font-semibold{{else}}hover:bg-ink/5{{end}}">{{$p}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 text-xs text-[color:var(--ink)]/50">
|
<div class="mt-5 text-xs text-muted">
|
||||||
Current: <code data-demo-current class="font-semibold">{{.Variant}} / {{.Intensity}} / {{.PhaseSel}}</code>
|
Current: <code data-demo-current class="font-semibold">{{.Variant}} / {{.Intensity}} / {{.PhaseSel}}</code>
|
||||||
<span class="mx-1">·</span> Tip: aurora and clear night want the night phase; wind and haze are the new autumn gusts and Saharan calima.
|
<span class="mx-1">·</span> Tip: aurora and clear night want the night phase; wind and haze are the new autumn gusts and Saharan calima.
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="rounded-3xl bg-[color:var(--card)] p-10 shadow-pete border-2 border-[color:var(--ink)]/10 text-center">
|
<section class="rounded-3xl bg-card p-10 shadow-pete border-2 border-ink/10 text-center">
|
||||||
<p class="font-display text-lg">Sample card</p>
|
<p class="font-display text-lg">Sample card</p>
|
||||||
<p class="mt-2 text-sm text-[color:var(--ink)]/60">Particles should drift in the margins around this block, not over the top of it.</p>
|
<p class="mt-2 text-sm text-muted">Particles should drift in the margins around this block, not over the top of it.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="grid gap-4 sm:grid-cols-2">
|
<section class="grid gap-4 sm:grid-cols-2">
|
||||||
<div class="rounded-3xl bg-[color:var(--card)] p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<div class="rounded-3xl bg-card p-6 shadow-pete border-2 border-ink/10">
|
||||||
<p class="font-display">Card A</p>
|
<p class="font-display">Card A</p>
|
||||||
<p class="text-sm text-[color:var(--ink)]/60 mt-1">Two cards with a gap so you can see the canvas between them.</p>
|
<p class="text-sm text-muted mt-1">Two cards with a gap so you can see the canvas between them.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-3xl bg-[color:var(--card)] p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
|
<div class="rounded-3xl bg-card p-6 shadow-pete border-2 border-ink/10">
|
||||||
<p class="font-display">Card B</p>
|
<p class="font-display">Card B</p>
|
||||||
<p class="text-sm text-[color:var(--ink)]/60 mt-1">Make your window taller to give the particles more room.</p>
|
<p class="text-sm text-muted mt-1">Make your window taller to give the particles more room.</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
function markActive(attr, value) {
|
function markActive(attr, value) {
|
||||||
document.querySelectorAll("[" + attr + "]").forEach(function (el) {
|
document.querySelectorAll("[" + attr + "]").forEach(function (el) {
|
||||||
var on = el.getAttribute(attr) === value;
|
var on = el.getAttribute(attr) === value;
|
||||||
el.classList.toggle("bg-[color:var(--accent)]", on);
|
el.classList.toggle("bg-accent", on);
|
||||||
el.classList.toggle("text-white", on);
|
el.classList.toggle("text-white", on);
|
||||||
el.classList.toggle("font-semibold", on);
|
el.classList.toggle("font-semibold", on);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,16 +9,16 @@
|
|||||||
<li>
|
<li>
|
||||||
<div class="flex items-baseline justify-between gap-3">
|
<div class="flex items-baseline justify-between gap-3">
|
||||||
<span class="flex-1 font-semibold">{{.Name}}{{if .Temper}} <span class="text-theme-adventure">+{{.Temper}}</span>{{end}}</span>
|
<span class="flex-1 font-semibold">{{.Name}}{{if .Temper}} <span class="text-theme-adventure">+{{.Temper}}</span>{{end}}</span>
|
||||||
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">{{if .Tier}}T{{.Tier}}{{end}}{{if .Value}}{{if .Tier}} · {{end}}{{.Value}}g{{end}}</span>
|
<span class="text-xs text-muted shrink-0">{{if .Tier}}T{{.Tier}}{{end}}{{if .Value}}{{if .Tier}} · {{end}}{{.Value}}g{{end}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{if or .Slot .SkillSource .Attunement}}
|
{{if or .Slot .SkillSource .Attunement}}
|
||||||
<div class="flex flex-wrap gap-1.5 mt-1">
|
<div class="flex flex-wrap gap-1.5 mt-1">
|
||||||
{{if .Slot}}<span class="text-[11px] rounded-full bg-[color:var(--ink)]/5 px-2 py-0.5 text-[color:var(--ink)]/60">{{.Slot}}</span>{{end}}
|
{{if .Slot}}<span class="text-[11px] rounded-full bg-ink/5 px-2 py-0.5 text-muted">{{.Slot}}</span>{{end}}
|
||||||
{{if .SkillSource}}<span class="text-[11px] rounded-full bg-[color:var(--ink)]/5 px-2 py-0.5 text-[color:var(--ink)]/60">{{.SkillSource}}</span>{{end}}
|
{{if .SkillSource}}<span class="text-[11px] rounded-full bg-ink/5 px-2 py-0.5 text-muted">{{.SkillSource}}</span>{{end}}
|
||||||
{{if .Attunement}}{{if .Attuned}}<span class="text-[11px] rounded-full bg-theme-adventure/20 px-2 py-0.5 text-theme-adventure font-semibold">bonded</span>{{else if .Worn}}<span class="text-[11px] rounded-full bg-amber-400/20 px-2 py-0.5 text-[color:var(--warn)] font-semibold">inert · no bond free</span>{{else}}<span class="text-[11px] rounded-full bg-[color:var(--ink)]/10 px-2 py-0.5 text-[color:var(--ink)]/60">needs a bond</span>{{end}}{{end}}
|
{{if .Attunement}}{{if .Attuned}}<span class="text-[11px] rounded-full bg-theme-adventure/20 px-2 py-0.5 text-theme-adventure font-semibold">bonded</span>{{else if .Worn}}<span class="text-[11px] rounded-full bg-amber-400/20 px-2 py-0.5 text-warn font-semibold">inert · no bond free</span>{{else}}<span class="text-[11px] rounded-full bg-ink/10 px-2 py-0.5 text-muted">needs a bond</span>{{end}}{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .Desc}}<p class="text-xs text-[color:var(--ink)]/55 mt-1 leading-snug">{{.Desc}}</p>{{end}}
|
{{if .Desc}}<p class="text-xs text-muted mt-1 leading-snug">{{.Desc}}</p>{{end}}
|
||||||
{{if .Effect}}<p class="text-xs text-theme-adventure/80 mt-0.5">{{.Effect}}</p>{{end}}
|
{{if .Effect}}<p class="text-xs text-theme-adventure/80 mt-0.5">{{.Effect}}</p>{{end}}
|
||||||
{{/* The compare card: how this backpack item stacks up against what's worn in
|
{{/* The compare card: how this backpack item stacks up against what's worn in
|
||||||
the slot it would equip into. gogobee decided the verdict and the per-stat
|
the slot it would equip into. gogobee decided the verdict and the per-stat
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
{{else if eq .Verdict "same"}}<span class="cmp-chip cmp-same">= no change</span>{{end}}
|
{{else if eq .Verdict "same"}}<span class="cmp-chip cmp-same">= no change</span>{{end}}
|
||||||
{{range .Deltas}}<span class="cmp-delta {{if .Better}}cmp-delta-up{{else}}cmp-delta-down{{end}}">{{.Text}}</span>{{end}}
|
{{range .Deltas}}<span class="cmp-delta {{if .Better}}cmp-delta-up{{else}}cmp-delta-down{{end}}">{{.Text}}</span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if .VsName}}<p class="text-[11px] text-[color:var(--ink)]/45 mt-0.5">vs worn {{.VsName}}</p>{{end}}
|
{{if .VsName}}<p class="text-[11px] text-muted mt-0.5">vs worn {{.VsName}}</p>{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .EquipAction}}
|
{{if .EquipAction}}
|
||||||
<div class="mt-1.5">
|
<div class="mt-1.5">
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
<article class="mt-2 mb-10 max-w-3xl mx-auto" id="who" data-token="{{.Mark.Token}}">
|
<article class="mt-2 mb-10 max-w-3xl mx-auto" id="who" data-token="{{.Mark.Token}}">
|
||||||
<nav class="mb-4">
|
<nav class="mb-4">
|
||||||
<a href="/adventure" class="inline-flex items-center gap-1.5 text-sm font-semibold text-[color:var(--ink)]/60 hover:text-[color:var(--ink)] transition">
|
<a href="/adventure" class="inline-flex items-center gap-1.5 text-sm font-semibold text-muted hover:text-ink transition">
|
||||||
<span aria-hidden="true">←</span> The board
|
<span aria-hidden="true">←</span> The board
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -84,35 +84,35 @@
|
|||||||
{{if .HasDetail}}
|
{{if .HasDetail}}
|
||||||
<!-- Stats + gear: public, the same anonymity model as the board. -->
|
<!-- Stats + gear: public, the same anonymity model as the board. -->
|
||||||
<section class="mt-8 grid gap-6 sm:grid-cols-2">
|
<section class="mt-8 grid gap-6 sm:grid-cols-2">
|
||||||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
<div class="rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete">
|
||||||
<h2 class="font-display text-xl font-bold mb-4">Vitals</h2>
|
<h2 class="font-display text-xl font-bold mb-4">Vitals</h2>
|
||||||
<div class="flex items-baseline justify-between">
|
<div class="flex items-baseline justify-between">
|
||||||
<span class="text-sm uppercase tracking-wider text-[color:var(--ink)]/50">Hit points</span>
|
<span class="text-sm uppercase tracking-wider text-muted">Hit points</span>
|
||||||
<span class="font-semibold" id="who-hp">{{.Detail.HPCurrent}} / {{.Detail.HPMax}}{{if .Detail.TempHP}} <span class="text-theme-adventure">+{{.Detail.TempHP}}</span>{{end}}</span>
|
<span class="font-semibold" id="who-hp">{{.Detail.HPCurrent}} / {{.Detail.HPMax}}{{if .Detail.TempHP}} <span class="text-theme-adventure">+{{.Detail.TempHP}}</span>{{end}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 h-2 rounded-full bg-[color:var(--ink)]/10 overflow-hidden">
|
<div class="mt-2 h-2 rounded-full bg-ink/10 overflow-hidden">
|
||||||
<div class="h-full rounded-full bg-theme-adventure transition-all" id="who-hp-bar" style="width:0%"></div>
|
<div class="h-full rounded-full bg-theme-adventure transition-all" id="who-hp-bar" style="width:0%"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex items-baseline justify-between">
|
<div class="mt-4 flex items-baseline justify-between">
|
||||||
<span class="text-sm uppercase tracking-wider text-[color:var(--ink)]/50">Armor class</span>
|
<span class="text-sm uppercase tracking-wider text-muted">Armor class</span>
|
||||||
<span class="font-semibold" id="who-ac">{{.Detail.ArmorClass}}</span>
|
<span class="font-semibold" id="who-ac">{{.Detail.ArmorClass}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 grid grid-cols-3 gap-2">
|
<div class="mt-5 grid grid-cols-3 gap-2">
|
||||||
{{range .Abilities}}
|
{{range .Abilities}}
|
||||||
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
|
<div class="rounded-2xl bg-ink/5 px-2 py-3 text-center">
|
||||||
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50">{{.Label}}</div>
|
<div class="text-[10px] uppercase tracking-wider text-muted">{{.Label}}</div>
|
||||||
<div class="font-display text-lg font-bold leading-none mt-1">{{.Score}}</div>
|
<div class="font-display text-lg font-bold leading-none mt-1">{{.Score}}</div>
|
||||||
<div class="text-xs text-[color:var(--ink)]/60">{{.ModStr}}</div>
|
<div class="text-xs text-muted">{{.ModStr}}</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .Mark.OnRun}}
|
{{if .Mark.OnRun}}
|
||||||
<div class="mt-5 pt-4 border-t border-[color:var(--ink)]/10 space-y-1.5 text-sm" id="who-expedition">
|
<div class="mt-5 pt-4 border-t border-ink/10 space-y-1.5 text-sm" id="who-expedition">
|
||||||
{{if .Detail.Room}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Room</span><span class="font-semibold" id="who-room">{{.Detail.Room}}</span></div>{{end}}
|
{{if .Detail.Room}}<div class="flex justify-between"><span class="text-muted">Room</span><span class="font-semibold" id="who-room">{{.Detail.Room}}</span></div>{{end}}
|
||||||
<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Supplies</span><span class="font-semibold" id="who-supplies">{{.Detail.Supplies}}</span></div>
|
<div class="flex justify-between"><span class="text-muted">Supplies</span><span class="font-semibold" id="who-supplies">{{.Detail.Supplies}}</span></div>
|
||||||
{{if .Detail.ThreatLevel}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Threat</span><span class="font-semibold" id="who-threat">{{.Detail.ThreatLevel}}</span></div>{{end}}
|
{{if .Detail.ThreatLevel}}<div class="flex justify-between"><span class="text-muted">Threat</span><span class="font-semibold" id="who-threat">{{.Detail.ThreatLevel}}</span></div>{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
@@ -123,8 +123,8 @@
|
|||||||
An unnamed seat is an opted-out player, kept on purpose — see
|
An unnamed seat is an opted-out player, kept on purpose — see
|
||||||
partySeat.Anonymous for why it is not simply dropped. */}}
|
partySeat.Anonymous for why it is not simply dropped. */}}
|
||||||
{{if .Detail.Party}}
|
{{if .Detail.Party}}
|
||||||
<div class="mt-5 pt-4 border-t border-[color:var(--ink)]/10">
|
<div class="mt-5 pt-4 border-t border-ink/10">
|
||||||
<h3 class="text-sm uppercase tracking-wider text-[color:var(--ink)]/50 mb-1">Down there together</h3>
|
<h3 class="text-sm uppercase tracking-wider text-muted mb-1">Down there together</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{{range .Detail.Party}}
|
{{range .Detail.Party}}
|
||||||
<li class="party-seat">
|
<li class="party-seat">
|
||||||
@@ -136,7 +136,7 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
<span class="flex-1 text-sm font-semibold">{{.Name}}</span>
|
<span class="flex-1 text-sm font-semibold">{{.Name}}</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .Level}}<span class="text-xs text-[color:var(--ink)]/50 shrink-0">lv {{.Level}}</span>{{end}}
|
{{if .Level}}<span class="text-xs text-muted shrink-0">lv {{.Level}}</span>{{end}}
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -148,20 +148,20 @@
|
|||||||
actually render (it needs pushed Slots) — otherwise an owner whose detail
|
actually render (it needs pushed Slots) — otherwise an owner whose detail
|
||||||
predates ask 7's Slots would see no standard gear at all. */}}
|
predates ask 7's Slots would see no standard gear at all. */}}
|
||||||
{{if not .Self.Slots}}
|
{{if not .Self.Slots}}
|
||||||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
<div class="rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete">
|
||||||
<h2 class="font-display text-xl font-bold mb-4">Gear</h2>
|
<h2 class="font-display text-xl font-bold mb-4">Gear</h2>
|
||||||
{{if .Detail.Gear}}
|
{{if .Detail.Gear}}
|
||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
{{range .Detail.Gear}}
|
{{range .Detail.Gear}}
|
||||||
<li class="flex items-baseline justify-between gap-3">
|
<li class="flex items-baseline justify-between gap-3">
|
||||||
<span class="text-xs uppercase tracking-wider text-[color:var(--ink)]/45 w-16 shrink-0">{{.Slot}}</span>
|
<span class="text-xs uppercase tracking-wider text-muted w-16 shrink-0">{{.Slot}}</span>
|
||||||
<span class="font-semibold flex-1">{{.Name}}{{if .Masterwork}} <span title="masterwork">★</span>{{end}}</span>
|
<span class="font-semibold flex-1">{{.Name}}{{if .Masterwork}} <span title="masterwork">★</span>{{end}}</span>
|
||||||
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">T{{.Tier}}{{if .Condition}} · {{.Condition}}%{{end}}</span>
|
<span class="text-xs text-muted shrink-0">T{{.Tier}}{{if .Condition}} · {{.Condition}}%{{end}}</span>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="text-sm text-[color:var(--ink)]/50">Traveling light — nothing equipped.</p>
|
<p class="text-sm text-muted">Traveling light — nothing equipped.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -172,15 +172,15 @@
|
|||||||
full, the doors leading out of them, and the unopened rooms behind those
|
full, the doors leading out of them, and the unopened rooms behind those
|
||||||
doors as question marks. Pete lays it out; it is never told what a room
|
doors as question marks. Pete lays it out; it is never told what a room
|
||||||
holds, only that a door is there. -->
|
holds, only that a door is there. -->
|
||||||
<section class="mt-6 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
<section class="mt-6 rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete">
|
||||||
<div class="flex items-baseline justify-between mb-4">
|
<div class="flex items-baseline justify-between mb-4">
|
||||||
<h2 class="font-display text-xl font-bold">The map</h2>
|
<h2 class="font-display text-xl font-bold">The map</h2>
|
||||||
{{if .Detail.Room}}<span class="text-sm text-[color:var(--ink)]/50">Room <span class="font-semibold text-[color:var(--ink)]/70">{{.Detail.Room}}</span></span>{{end}}
|
{{if .Detail.Room}}<span class="text-sm text-muted">Room <span class="font-semibold text-muted">{{.Detail.Room}}</span></span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
{{template "dungeonmap" .MapView}}
|
{{template "dungeonmap" .MapView}}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex flex-wrap gap-x-4 gap-y-1.5 text-xs text-[color:var(--ink)]/55">
|
<div class="mt-4 flex flex-wrap gap-x-4 gap-y-1.5 text-xs text-muted">
|
||||||
<span class="inline-flex items-center gap-1.5"><span class="map-dot map-node-entry"></span>entrance</span>
|
<span class="inline-flex items-center gap-1.5"><span class="map-dot map-node-entry"></span>entrance</span>
|
||||||
<span class="inline-flex items-center gap-1.5"><span class="map-dot map-node-boss"></span>boss</span>
|
<span class="inline-flex items-center gap-1.5"><span class="map-dot map-node-boss"></span>boss</span>
|
||||||
<span class="inline-flex items-center gap-1.5"><span class="map-dot map-node-unknown"></span>unexplored</span>
|
<span class="inline-flex items-center gap-1.5"><span class="map-dot map-node-unknown"></span>unexplored</span>
|
||||||
@@ -189,8 +189,8 @@
|
|||||||
</section>
|
</section>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<section class="mt-8 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
<section class="mt-8 rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete">
|
||||||
<p class="text-sm text-[color:var(--ink)]/60">No detailed sheet on file for this adventurer yet — check back after the next snapshot.</p>
|
<p class="text-sm text-muted">No detailed sheet on file for this adventurer yet — check back after the next snapshot.</p>
|
||||||
</section>
|
</section>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
@@ -199,10 +199,10 @@
|
|||||||
own channel and can outlive the snapshot that drew the map — a run that
|
own channel and can outlive the snapshot that drew the map — a run that
|
||||||
just ended still has a story after the board has moved the mark back to
|
just ended still has a story after the board has moved the mark back to
|
||||||
town. Rebuilt in place by the same poll that moves the HP bar. -->
|
town. Rebuilt in place by the same poll that moves the HP bar. -->
|
||||||
<section id="who-runlog-section" class="mt-6 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete{{if not .RunLog.Has}} hidden{{end}}">
|
<section id="who-runlog-section" class="mt-6 rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete{{if not .RunLog.Has}} hidden{{end}}">
|
||||||
<div class="flex items-baseline justify-between mb-4 gap-3">
|
<div class="flex items-baseline justify-between mb-4 gap-3">
|
||||||
<h2 class="font-display text-xl font-bold">The run</h2>
|
<h2 class="font-display text-xl font-bold">The run</h2>
|
||||||
<span id="who-runlog-status" class="text-sm text-[color:var(--ink)]/50">{{if .RunLog.Live}}under way{{else if .RunLog.Outcome}}{{.RunLog.Outcome}}{{else}}finished{{end}}{{if .RunLog.Rooms}} · room {{.RunLog.Rooms}}{{end}}</span>
|
<span id="who-runlog-status" class="text-sm text-muted">{{if .RunLog.Live}}under way{{else if .RunLog.Outcome}}{{.RunLog.Outcome}}{{else}}finished{{end}}{{if .RunLog.Rooms}} · room {{.RunLog.Rooms}}{{end}}</span>
|
||||||
</div>
|
</div>
|
||||||
<ol id="who-runlog" class="runlog">
|
<ol id="who-runlog" class="runlog">
|
||||||
{{range .RunLog.Lines}}
|
{{range .RunLog.Lines}}
|
||||||
@@ -230,10 +230,10 @@
|
|||||||
would be the page refusing an action the game would have allowed. gogobee
|
would be the page refusing an action the game would have allowed. gogobee
|
||||||
answers rejected_not_running if it really has ended, and that answer shows
|
answers rejected_not_running if it really has ended, and that answer shows
|
||||||
up in the strip below. -->
|
up in the strip below. -->
|
||||||
<section id="adv-actions" class="adv-actions mt-6 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete"
|
<section id="adv-actions" class="adv-actions mt-6 rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete"
|
||||||
data-balance="{{.Self.Balance}}">
|
data-balance="{{.Self.Balance}}">
|
||||||
<h2 class="font-display text-xl font-bold mb-1">Your call</h2>
|
<h2 class="font-display text-xl font-bold mb-1">Your call</h2>
|
||||||
<p class="text-sm text-[color:var(--ink)]/60 mb-4">
|
<p class="text-sm text-muted mb-4">
|
||||||
Asked for here, done on the game box. It picks these up within a few seconds.
|
Asked for here, done on the game box. It picks these up within a few seconds.
|
||||||
</p>
|
</p>
|
||||||
{{/* data-offer marks what stops being true once the extraction lands: there
|
{{/* data-offer marks what stops being true once the extraction lands: there
|
||||||
@@ -271,7 +271,7 @@
|
|||||||
{{if .CanAbandon}}
|
{{if .CanAbandon}}
|
||||||
<div data-offer class="mt-3">
|
<div data-offer class="mt-3">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="adv-action-btn rounded-full border border-[color:var(--warn)]/40 text-[color:var(--warn)] hover:bg-[color:var(--warn)]/10 px-4 py-1.5 text-sm font-semibold transition-colors"
|
class="adv-action-btn rounded-full border border-warn/40 text-warn hover:bg-warn/10 px-4 py-1.5 text-sm font-semibold transition-colors"
|
||||||
data-action="expedition_abandon"
|
data-action="expedition_abandon"
|
||||||
data-label="Call the whole thing off"
|
data-label="Call the whole thing off"
|
||||||
data-confirm-tone="warn"
|
data-confirm-tone="warn"
|
||||||
@@ -283,7 +283,7 @@
|
|||||||
{{if .CanLeave}}
|
{{if .CanLeave}}
|
||||||
<div data-offer class="mt-3">
|
<div data-offer class="mt-3">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="adv-action-btn rounded-full border border-[color:var(--ink)]/25 text-[color:var(--ink)]/70 hover:bg-[color:var(--ink)]/10 px-4 py-1.5 text-sm font-semibold transition-colors"
|
class="adv-action-btn rounded-full border border-ink/25 text-muted hover:bg-ink/10 px-4 py-1.5 text-sm font-semibold transition-colors"
|
||||||
data-action="expedition_leave"
|
data-action="expedition_leave"
|
||||||
data-label="Turn back alone"
|
data-label="Turn back alone"
|
||||||
data-confirm-label="Yes, turn back"
|
data-confirm-label="Yes, turn back"
|
||||||
@@ -301,11 +301,11 @@
|
|||||||
{{/* The way back in, first: it is the only offer here with a deadline on it,
|
{{/* The way back in, first: it is the only offer here with a deadline on it,
|
||||||
and it is the answer to the verdict "Pull out" leaves behind, which until
|
and it is the answer to the verdict "Pull out" leaves behind, which until
|
||||||
now told a web player to go and type !resume in Matrix. */}}
|
now told a web player to go and type !resume in Matrix. */}}
|
||||||
<div data-offer class="mt-5 pt-5 border-t border-[color:var(--ink)]/10">
|
<div data-offer class="mt-5 pt-5 border-t border-ink/10">
|
||||||
<h3 class="font-display text-base font-bold">Go back in</h3>
|
<h3 class="font-display text-base font-bold">Go back in</h3>
|
||||||
<p class="text-sm text-[color:var(--ink)]/60 mt-0.5 mb-2.5">
|
<p class="text-sm text-muted mt-0.5 mb-2.5">
|
||||||
{{.Self.Resume.Display}}, day {{.Self.Resume.Day}} — waiting where you left it.
|
{{.Self.Resume.Display}}, day {{.Self.Resume.Day}} — waiting where you left it.
|
||||||
{{with untilUnix .Self.Resume.ExpiresAt}}<span class="text-[color:var(--warn)] font-semibold">{{.}}</span>.{{end}}
|
{{with untilUnix .Self.Resume.ExpiresAt}}<span class="text-warn font-semibold">{{.}}</span>.{{end}}
|
||||||
You re-stock before you go, so pick a pack.
|
You re-stock before you go, so pick a pack.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-wrap gap-1.5">
|
<div class="flex flex-wrap gap-1.5">
|
||||||
@@ -328,15 +328,15 @@
|
|||||||
{{/* Set out. The zone list is already level-gated and postgame-gated by
|
{{/* Set out. The zone list is already level-gated and postgame-gated by
|
||||||
gogobee, and it arrives EMPTY while this adventurer is already out —
|
gogobee, and it arrives EMPTY while this adventurer is already out —
|
||||||
which is why there is no "you're on an expedition" branch here. */}}
|
which is why there is no "you're on an expedition" branch here. */}}
|
||||||
<div data-offer class="mt-5 pt-5 border-t border-[color:var(--ink)]/10">
|
<div data-offer class="mt-5 pt-5 border-t border-ink/10">
|
||||||
<h3 class="font-display text-base font-bold">Set out</h3>
|
<h3 class="font-display text-base font-bold">Set out</h3>
|
||||||
<p class="text-sm text-[color:var(--ink)]/60 mt-0.5 mb-2.5">
|
<p class="text-sm text-muted mt-0.5 mb-2.5">
|
||||||
Pick where, then pick how much to carry. The pack is what you pay for.
|
Pick where, then pick how much to carry. The pack is what you pay for.
|
||||||
</p>
|
</p>
|
||||||
<label class="block">
|
<label class="block">
|
||||||
<span class="sr-only">Zone</span>
|
<span class="sr-only">Zone</span>
|
||||||
<select id="adv-zone-pick"
|
<select id="adv-zone-pick"
|
||||||
class="w-full rounded-xl bg-[color:var(--ink)]/5 border border-[color:var(--ink)]/15 px-3 py-2 text-sm font-semibold">
|
class="w-full rounded-xl bg-ink/5 border border-ink/15 px-3 py-2 text-sm font-semibold">
|
||||||
{{range .Self.Zones}}
|
{{range .Self.Zones}}
|
||||||
<option value="{{.ID}}">{{.Display}} · T{{.Tier}}{{if .Postgame}} · mythic{{end}}</option>
|
<option value="{{.ID}}">{{.Display}} · T{{.Tier}}{{if .Postgame}} · mythic{{end}}</option>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -348,7 +348,7 @@
|
|||||||
{{range .Self.Zones}}
|
{{range .Self.Zones}}
|
||||||
{{$z := .}}
|
{{$z := .}}
|
||||||
<div class="adv-zone-loadouts mt-2.5 hidden" data-zone="{{$z.ID}}">
|
<div class="adv-zone-loadouts mt-2.5 hidden" data-zone="{{$z.ID}}">
|
||||||
{{with $z.Hook}}<p class="text-xs italic text-[color:var(--ink)]/50 mb-2">{{.}}</p>{{end}}
|
{{with $z.Hook}}<p class="text-xs italic text-muted mb-2">{{.}}</p>{{end}}
|
||||||
<div class="flex flex-wrap gap-1.5">
|
<div class="flex flex-wrap gap-1.5">
|
||||||
{{range $z.Loadouts}}
|
{{range $z.Loadouts}}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
@@ -368,20 +368,20 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .Self.Babysit}}
|
{{if .Self.Babysit}}
|
||||||
<div data-offer class="mt-5 pt-5 border-t border-[color:var(--ink)]/10">
|
<div data-offer class="mt-5 pt-5 border-t border-ink/10">
|
||||||
<h3 class="font-display text-base font-bold">The sitter</h3>
|
<h3 class="font-display text-base font-bold">The sitter</h3>
|
||||||
{{if .Self.Babysit.Active}}
|
{{if .Self.Babysit.Active}}
|
||||||
{{/* Engaged: say so, and offer only the way back out. Buying a second one
|
{{/* Engaged: say so, and offer only the way back out. Buying a second one
|
||||||
is refused by the game anyway, and a buy button under "already engaged"
|
is refused by the game anyway, and a buy button under "already engaged"
|
||||||
reads as a page that has not noticed. */}}
|
reads as a page that has not noticed. */}}
|
||||||
<p class="text-sm text-[color:var(--ink)]/60 mt-0.5">
|
<p class="text-sm text-muted mt-0.5">
|
||||||
Somebody is looking after the camp{{with untilUnix .Self.Babysit.ExpiresAt}} — {{.}}{{end}}.
|
Somebody is looking after the camp{{with untilUnix .Self.Babysit.ExpiresAt}} — {{.}}{{end}}.
|
||||||
Your pet is being tended daily and standard camps rest like fortified ones.
|
Your pet is being tended daily and standard camps rest like fortified ones.
|
||||||
</p>
|
</p>
|
||||||
{{if .CanCancelSitter}}
|
{{if .CanCancelSitter}}
|
||||||
<div class="flex flex-wrap gap-1.5 mt-2.5">
|
<div class="flex flex-wrap gap-1.5 mt-2.5">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="adv-action-btn rounded-full border border-[color:var(--ink)]/25 text-[color:var(--ink)]/70 hover:bg-[color:var(--ink)]/10 px-3.5 py-1.5 text-sm font-semibold transition-colors"
|
class="adv-action-btn rounded-full border border-ink/25 text-muted hover:bg-ink/10 px-3.5 py-1.5 text-sm font-semibold transition-colors"
|
||||||
data-action="babysit_cancel"
|
data-action="babysit_cancel"
|
||||||
data-label="Send them home"
|
data-label="Send them home"
|
||||||
data-confirm-label="Yes, send them home"
|
data-confirm-label="Yes, send them home"
|
||||||
@@ -389,7 +389,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="text-sm text-[color:var(--ink)]/60 mt-0.5 mb-2.5">
|
<p class="text-sm text-muted mt-0.5 mb-2.5">
|
||||||
Pet tended daily, standard camps rest like fortified ones, rival duels declined for you.
|
Pet tended daily, standard camps rest like fortified ones, rival duels declined for you.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-wrap gap-1.5">
|
<div class="flex flex-wrap gap-1.5">
|
||||||
@@ -425,29 +425,29 @@
|
|||||||
same information the /adventure feed already printed, only as numbers
|
same information the /adventure feed already printed, only as numbers
|
||||||
instead of as forty separate sentences. Not a gogobee snapshot: Pete
|
instead of as forty separate sentences. Not a gogobee snapshot: Pete
|
||||||
counted these itself out of the facts it kept. -->
|
counted these itself out of the facts it kept. -->
|
||||||
<section class="mt-8 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
<section class="mt-8 rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete">
|
||||||
<h2 class="font-display text-xl font-bold mb-4">The record</h2>
|
<h2 class="font-display text-xl font-bold mb-4">The record</h2>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-5 gap-2">
|
<div class="grid grid-cols-2 sm:grid-cols-5 gap-2">
|
||||||
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
|
<div class="rounded-2xl bg-ink/5 px-2 py-3 text-center">
|
||||||
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.BossKills}}</div>
|
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.BossKills}}</div>
|
||||||
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50 mt-1.5">bosses down</div>
|
<div class="text-[10px] uppercase tracking-wider text-muted mt-1.5">bosses down</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
|
<div class="rounded-2xl bg-ink/5 px-2 py-3 text-center">
|
||||||
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.ZoneClears}}</div>
|
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.ZoneClears}}</div>
|
||||||
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50 mt-1.5">zones cleared</div>
|
<div class="text-[10px] uppercase tracking-wider text-muted mt-1.5">zones cleared</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
|
<div class="rounded-2xl bg-ink/5 px-2 py-3 text-center">
|
||||||
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.TreasuresFound}}</div>
|
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.TreasuresFound}}</div>
|
||||||
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50 mt-1.5">treasure{{if ne .Trophies.TreasuresFound 1}}s{{end}}</div>
|
<div class="text-[10px] uppercase tracking-wider text-muted mt-1.5">treasure{{if ne .Trophies.TreasuresFound 1}}s{{end}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
|
<div class="rounded-2xl bg-ink/5 px-2 py-3 text-center">
|
||||||
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.Deaths}}</div>
|
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.Deaths}}</div>
|
||||||
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50 mt-1.5">death{{if ne .Trophies.Deaths 1}}s{{end}}</div>
|
<div class="text-[10px] uppercase tracking-wider text-muted mt-1.5">death{{if ne .Trophies.Deaths 1}}s{{end}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
|
<div class="rounded-2xl bg-ink/5 px-2 py-3 text-center">
|
||||||
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.Retreats}}</div>
|
<div class="font-display text-2xl font-bold leading-none">{{.Trophies.Retreats}}</div>
|
||||||
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50 mt-1.5">walked out</div>
|
<div class="text-[10px] uppercase tracking-wider text-muted mt-1.5">walked out</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -465,7 +465,7 @@
|
|||||||
{{range .Trophies.Bosses}}
|
{{range .Trophies.Bosses}}
|
||||||
<li class="flex items-baseline justify-between gap-3">
|
<li class="flex items-baseline justify-between gap-3">
|
||||||
<span class="flex-1">{{.Boss}}{{if .First}} <span class="text-theme-adventure" title="first in the realm to clear it">★</span>{{end}}</span>
|
<span class="flex-1">{{.Boss}}{{if .First}} <span class="text-theme-adventure" title="first in the realm to clear it">★</span>{{end}}</span>
|
||||||
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">×{{.Kills}}</span>
|
<span class="text-xs text-muted shrink-0">×{{.Kills}}</span>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -478,8 +478,8 @@
|
|||||||
<ul class="space-y-1.5 text-sm max-h-56 overflow-y-auto pr-1">
|
<ul class="space-y-1.5 text-sm max-h-56 overflow-y-auto pr-1">
|
||||||
{{range .Trophies.Zones}}
|
{{range .Trophies.Zones}}
|
||||||
<li class="flex items-baseline justify-between gap-3">
|
<li class="flex items-baseline justify-between gap-3">
|
||||||
<span class="flex-1">{{.Zone}}{{if .First}} <span class="text-theme-adventure" title="first clear in the realm">★</span>{{end}}{{if .Region}} <span class="text-[color:var(--ink)]/45">{{.Region}}</span>{{end}}</span>
|
<span class="flex-1">{{.Zone}}{{if .First}} <span class="text-theme-adventure" title="first clear in the realm">★</span>{{end}}{{if .Region}} <span class="text-muted">{{.Region}}</span>{{end}}</span>
|
||||||
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">×{{.Clears}}</span>
|
<span class="text-xs text-muted shrink-0">×{{.Clears}}</span>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -488,13 +488,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .Trophies.Treasures}}
|
{{if .Trophies.Treasures}}
|
||||||
<div class="mt-6 pt-4 border-t border-[color:var(--ink)]/10">
|
<div class="mt-6 pt-4 border-t border-ink/10">
|
||||||
<h3 class="font-display text-lg font-bold mb-3">Treasures found</h3>
|
<h3 class="font-display text-lg font-bold mb-3">Treasures found</h3>
|
||||||
<ul class="space-y-1.5 text-sm max-h-56 overflow-y-auto pr-1">
|
<ul class="space-y-1.5 text-sm max-h-56 overflow-y-auto pr-1">
|
||||||
{{range .Trophies.Treasures}}
|
{{range .Trophies.Treasures}}
|
||||||
<li class="flex items-baseline justify-between gap-3">
|
<li class="flex items-baseline justify-between gap-3">
|
||||||
<span class="flex-1">💎 {{.Item}}{{if .First}} <span class="text-theme-adventure" title="first in the realm to pull this hoard">★</span>{{end}}</span>
|
<span class="flex-1">💎 {{.Item}}{{if .First}} <span class="text-theme-adventure" title="first in the realm to pull this hoard">★</span>{{end}}</span>
|
||||||
{{if .Zone}}<span class="text-xs text-[color:var(--ink)]/45 shrink-0">{{.Zone}}</span>{{end}}
|
{{if .Zone}}<span class="text-xs text-muted shrink-0">{{.Zone}}</span>{{end}}
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -502,7 +502,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .Trophies.Milestones}}
|
{{if .Trophies.Milestones}}
|
||||||
<div class="mt-6 pt-4 border-t border-[color:var(--ink)]/10">
|
<div class="mt-6 pt-4 border-t border-ink/10">
|
||||||
<h3 class="font-display text-lg font-bold mb-3">Milestones</h3>
|
<h3 class="font-display text-lg font-bold mb-3">Milestones</h3>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
{{range .Trophies.Milestones}}
|
{{range .Trophies.Milestones}}
|
||||||
@@ -515,24 +515,24 @@
|
|||||||
|
|
||||||
<!-- The trail: every fact we have, newest first, each linking to the dispatch
|
<!-- The trail: every fact we have, newest first, each linking to the dispatch
|
||||||
that told it. -->
|
that told it. -->
|
||||||
<section class="mt-8 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
<section class="mt-8 rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete">
|
||||||
<h2 class="font-display text-xl font-bold mb-4">The trail</h2>
|
<h2 class="font-display text-xl font-bold mb-4">The trail</h2>
|
||||||
<ol class="relative border-l-2 border-[color:var(--ink)]/10 ml-3 space-y-4">
|
<ol class="relative border-l-2 border-ink/10 ml-3 space-y-4">
|
||||||
{{range .Timeline}}
|
{{range .Timeline}}
|
||||||
<li class="relative pl-6">
|
<li class="relative pl-6">
|
||||||
<span class="absolute -left-[13px] top-0.5 flex h-6 w-6 items-center justify-center rounded-full bg-[color:var(--card)] border-2 {{if .Notable}}border-theme-adventure{{else}}border-[color:var(--ink)]/10{{end}} text-xs" aria-hidden="true">{{.Emoji}}</span>
|
<span class="absolute -left-[13px] top-0.5 flex h-6 w-6 items-center justify-center rounded-full bg-card border-2 {{if .Notable}}border-theme-adventure{{else}}border-ink/10{{end}} text-xs" aria-hidden="true">{{.Emoji}}</span>
|
||||||
<a href="{{.Permalink}}" class="group block">
|
<a href="{{.Permalink}}" class="group block">
|
||||||
<div class="flex items-baseline justify-between gap-3">
|
<div class="flex items-baseline justify-between gap-3">
|
||||||
<span class="font-semibold group-hover:text-theme-adventure transition {{if .Notable}}text-theme-adventure{{end}}">{{.Label}}</span>
|
<span class="font-semibold group-hover:text-theme-adventure transition {{if .Notable}}text-theme-adventure{{end}}">{{.Label}}</span>
|
||||||
<span class="text-xs text-[color:var(--ink)]/45 shrink-0">{{.When}}</span>
|
<span class="text-xs text-muted shrink-0">{{.When}}</span>
|
||||||
</div>
|
</div>
|
||||||
{{if .Line}}<p class="text-sm text-[color:var(--ink)]/60 mt-0.5">{{.Line}}</p>{{end}}
|
{{if .Line}}<p class="text-sm text-muted mt-0.5">{{.Line}}</p>{{end}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ol>
|
</ol>
|
||||||
{{if .MoreHistory}}
|
{{if .MoreHistory}}
|
||||||
<p class="mt-5 pt-4 border-t border-[color:var(--ink)]/10 text-xs text-[color:var(--ink)]/45">
|
<p class="mt-5 pt-4 border-t border-ink/10 text-xs text-muted">
|
||||||
Showing the most recent {{len .Timeline}}. The counts above cover everything.
|
Showing the most recent {{len .Timeline}}. The counts above cover everything.
|
||||||
</p>
|
</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -545,7 +545,7 @@
|
|||||||
<section class="mt-8">
|
<section class="mt-8">
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
<span class="rounded-full bg-theme-adventure text-white text-xs font-semibold px-3 py-1">Your adventurer</span>
|
<span class="rounded-full bg-theme-adventure text-white text-xs font-semibold px-3 py-1">Your adventurer</span>
|
||||||
<span class="text-xs text-[color:var(--ink)]/45">only you can see the panels below</span>
|
<span class="text-xs text-muted">only you can see the panels below</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{/* Equipment: the 5 standard slots, owner-only management. Take off round-trips
|
{{/* Equipment: the 5 standard slots, owner-only management. Take off round-trips
|
||||||
@@ -555,17 +555,17 @@
|
|||||||
resolved server-side — the buttons only carry what was pushed. */}}
|
resolved server-side — the buttons only carry what was pushed. */}}
|
||||||
{{if .Self.Slots}}
|
{{if .Self.Slots}}
|
||||||
<div id="equip-panel" data-token="{{.Mark.Token}}" data-balance="{{printf "%.2f" .Self.Balance}}"
|
<div id="equip-panel" data-token="{{.Mark.Token}}" data-balance="{{printf "%.2f" .Self.Balance}}"
|
||||||
class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete mb-6">
|
class="rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete mb-6">
|
||||||
<div class="flex items-baseline justify-between mb-4">
|
<div class="flex items-baseline justify-between mb-4">
|
||||||
<h2 class="font-display text-xl font-bold">Equipment</h2>
|
<h2 class="font-display text-xl font-bold">Equipment</h2>
|
||||||
<span class="text-xs text-[color:var(--ink)]/50">Balance <span class="font-semibold text-[color:var(--ink)]/70">€{{printf "%.2f" .Self.Balance}}</span></span>
|
<span class="text-xs text-muted">Balance <span class="font-semibold text-muted">€{{printf "%.2f" .Self.Balance}}</span></span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="grid gap-3 sm:grid-cols-2">
|
<ul class="grid gap-3 sm:grid-cols-2">
|
||||||
{{range .Self.Slots}}
|
{{range .Self.Slots}}
|
||||||
<li class="slot-card rounded-2xl bg-[color:var(--ink)]/5 p-4">
|
<li class="slot-card rounded-2xl bg-ink/5 p-4">
|
||||||
<div class="flex items-baseline justify-between gap-2">
|
<div class="flex items-baseline justify-between gap-2">
|
||||||
<span class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/45">{{.Slot}}</span>
|
<span class="text-[10px] uppercase tracking-wider text-muted">{{.Slot}}</span>
|
||||||
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">T{{.Tier}}{{if .Condition}} · {{.Condition}}%{{end}}</span>
|
<span class="text-xs text-muted shrink-0">T{{.Tier}}{{if .Condition}} · {{.Condition}}%{{end}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="font-semibold mt-0.5 flex items-center flex-wrap gap-1.5">
|
<div class="font-semibold mt-0.5 flex items-center flex-wrap gap-1.5">
|
||||||
<span>{{.Name}}</span>
|
<span>{{.Name}}</span>
|
||||||
@@ -578,7 +578,7 @@
|
|||||||
data-action="unequip" data-slot="{{.Slot}}" data-item-name="{{.Name}}">Take off</button>{{end}}
|
data-action="unequip" data-slot="{{.Slot}}" data-item-name="{{.Name}}">Take off</button>{{end}}
|
||||||
{{if .NextTier}}<button type="button" class="equip-btn text-[11px] rounded-full border border-theme-adventure/40 text-theme-adventure hover:bg-theme-adventure/10 px-2.5 py-0.5 font-semibold transition-colors"
|
{{if .NextTier}}<button type="button" class="equip-btn text-[11px] rounded-full border border-theme-adventure/40 text-theme-adventure hover:bg-theme-adventure/10 px-2.5 py-0.5 font-semibold transition-colors"
|
||||||
data-action="upgrade" data-slot="{{.Slot}}" data-tier="{{.NextTier}}" data-cost="{{printf "%.2f" .NextPrice}}" data-item-name="{{.NextName}}">Upgrade to {{.NextName}} · €{{printf "%.0f" .NextPrice}}</button>{{end}}
|
data-action="upgrade" data-slot="{{.Slot}}" data-tier="{{.NextTier}}" data-cost="{{printf "%.2f" .NextPrice}}" data-item-name="{{.NextName}}">Upgrade to {{.NextName}} · €{{printf "%.0f" .NextPrice}}</button>{{end}}
|
||||||
{{if .RepairCost}}<button type="button" class="equip-btn text-[11px] rounded-full border border-[color:var(--warn)]/40 text-[color:var(--warn)] hover:bg-amber-400/10 px-2.5 py-0.5 font-semibold transition-colors"
|
{{if .RepairCost}}<button type="button" class="equip-btn text-[11px] rounded-full border border-warn/40 text-warn hover:bg-amber-400/10 px-2.5 py-0.5 font-semibold transition-colors"
|
||||||
data-action="repair" data-slot="{{.Slot}}" data-cost="{{.RepairCost}}" data-item-name="{{.Name}}">Repair · €{{.RepairCost}}</button>{{end}}
|
data-action="repair" data-slot="{{.Slot}}" data-cost="{{.RepairCost}}" data-item-name="{{.Name}}">Repair · €{{.RepairCost}}</button>{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -589,13 +589,13 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="grid gap-6 sm:grid-cols-2">
|
<div class="grid gap-6 sm:grid-cols-2">
|
||||||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
<div class="rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete">
|
||||||
<h2 class="font-display text-xl font-bold mb-4">Home</h2>
|
<h2 class="font-display text-xl font-bold mb-4">Home</h2>
|
||||||
<div class="space-y-1.5 text-sm">
|
<div class="space-y-1.5 text-sm">
|
||||||
<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">House tier</span><span class="font-semibold">{{if .Self.House.Tier}}{{.Self.House.Tier}}{{else}}no house{{end}}</span></div>
|
<div class="flex justify-between"><span class="text-muted">House tier</span><span class="font-semibold">{{if .Self.House.Tier}}{{.Self.House.Tier}}{{else}}no house{{end}}</span></div>
|
||||||
{{if .Self.House.LoanBalance}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Loan balance</span><span class="font-semibold">{{.Self.House.LoanBalance}}</span></div>{{end}}
|
{{if .Self.House.LoanBalance}}<div class="flex justify-between"><span class="text-muted">Loan balance</span><span class="font-semibold">{{.Self.House.LoanBalance}}</span></div>{{end}}
|
||||||
{{if .Self.House.Rate}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Rate</span><span class="font-semibold">{{.Self.House.Rate}}</span></div>{{end}}
|
{{if .Self.House.Rate}}<div class="flex justify-between"><span class="text-muted">Rate</span><span class="font-semibold">{{.Self.House.Rate}}</span></div>{{end}}
|
||||||
{{if .Self.House.Autopay}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Autopay</span><span class="font-semibold">on</span></div>{{end}}
|
{{if .Self.House.Autopay}}<div class="flex justify-between"><span class="text-muted">Autopay</span><span class="font-semibold">on</span></div>{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{/* Pets, with their levelling shown. They have been earning XP from every
|
{{/* Pets, with their levelling shown. They have been earning XP from every
|
||||||
@@ -610,27 +610,27 @@
|
|||||||
{{range .PetRows}}
|
{{range .PetRows}}
|
||||||
<li>
|
<li>
|
||||||
<div class="flex items-baseline justify-between gap-3">
|
<div class="flex items-baseline justify-between gap-3">
|
||||||
<span class="font-semibold flex-1">{{if .Name}}{{.Name}}{{else}}your {{.Type}}{{end}} <span class="text-[color:var(--ink)]/45 font-normal">{{.Type}}</span></span>
|
<span class="font-semibold flex-1">{{if .Name}}{{.Name}}{{else}}your {{.Type}}{{end}} <span class="text-muted font-normal">{{.Type}}</span></span>
|
||||||
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">lv {{.Level}}{{if .ArmorTier}} · barding T{{.ArmorTier}}{{end}}</span>
|
<span class="text-xs text-muted shrink-0">lv {{.Level}}{{if .ArmorTier}} · barding T{{.ArmorTier}}{{end}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pet-xp-track mt-1.5">
|
<div class="pet-xp-track mt-1.5">
|
||||||
<div class="pet-xp-fill{{if .Capped}} pet-xp-capped{{end}}" style="width: {{if .Capped}}100{{else}}{{.Percent}}{{end}}%"></div>
|
<div class="pet-xp-fill{{if .Capped}} pet-xp-capped{{end}}" style="width: {{if .Capped}}100{{else}}{{.Percent}}{{end}}%"></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1 text-xs text-[color:var(--ink)]/45">
|
<p class="mt-1 text-xs text-muted">
|
||||||
{{if .Capped}}fully grown{{else}}{{.Progress}} to level {{.NextLevel}}{{end}}
|
{{if .Capped}}fully grown{{else}}{{.Progress}} to level {{.NextLevel}}{{end}}
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="text-sm text-[color:var(--ink)]/50">No pets right now.</p>
|
<p class="text-sm text-muted">No pets right now.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="gear-panel" data-token="{{.Mark.Token}}" class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
<div id="gear-panel" data-token="{{.Mark.Token}}" class="rounded-3xl bg-card border-2 border-ink/10 p-6 shadow-pete">
|
||||||
{{if .Worn}}
|
{{if .Worn}}
|
||||||
<h2 class="font-display text-xl font-bold mb-1">Worn</h2>
|
<h2 class="font-display text-xl font-bold mb-1">Worn</h2>
|
||||||
<p class="text-xs text-[color:var(--ink)]/45 mb-3">{{.BondsUsed}} of 3 bonds in use</p>
|
<p class="text-xs text-muted mb-3">{{.BondsUsed}} of 3 bonds in use</p>
|
||||||
<ul class="space-y-2.5 text-sm">
|
<ul class="space-y-2.5 text-sm">
|
||||||
{{range .Worn}}{{template "itemrow" .}}{{end}}
|
{{range .Worn}}{{template "itemrow" .}}{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -643,7 +643,7 @@
|
|||||||
{{range .Backpack}}{{template "itemrow" .}}{{end}}
|
{{range .Backpack}}{{template "itemrow" .}}{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="text-sm text-[color:var(--ink)]/50">Backpack's empty.</p>
|
<p class="text-sm text-muted">Backpack's empty.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .VaultRows}}
|
{{if .VaultRows}}
|
||||||
@@ -842,8 +842,8 @@
|
|||||||
left.textContent = verb + ' ' + (o.item_name || o.slot || 'item');
|
left.textContent = verb + ' ' + (o.item_name || o.slot || 'item');
|
||||||
var right = document.createElement('span');
|
var right = document.createElement('span');
|
||||||
right.className = 'shrink-0 ' + (o.status === 'pending'
|
right.className = 'shrink-0 ' + (o.status === 'pending'
|
||||||
? 'text-[color:var(--ink)]/45'
|
? 'text-muted'
|
||||||
: (o.status === 'applied' ? 'text-theme-adventure font-semibold' : 'text-[color:var(--warn)]'));
|
: (o.status === 'applied' ? 'text-theme-adventure font-semibold' : 'text-warn'));
|
||||||
right.textContent = o.detail || STATUS[o.status] || o.status;
|
right.textContent = o.detail || STATUS[o.status] || o.status;
|
||||||
li.appendChild(left); li.appendChild(right);
|
li.appendChild(left); li.appendChild(right);
|
||||||
list.appendChild(li);
|
list.appendChild(li);
|
||||||
@@ -913,9 +913,9 @@
|
|||||||
var head = action === 'upgrade' ? ('Upgrade to ' + name) : ('Repair ' + name);
|
var head = action === 'upgrade' ? ('Upgrade to ' + name) : ('Repair ' + name);
|
||||||
|
|
||||||
var boxEl = document.createElement('div');
|
var boxEl = document.createElement('div');
|
||||||
boxEl.className = 'equip-confirm mt-2 rounded-xl bg-[color:var(--ink)]/5 p-2.5 text-xs';
|
boxEl.className = 'equip-confirm mt-2 rounded-xl bg-ink/5 p-2.5 text-xs';
|
||||||
var p = document.createElement('p');
|
var p = document.createElement('p');
|
||||||
p.className = 'text-[color:var(--ink)]/70';
|
p.className = 'text-muted';
|
||||||
p.textContent = head + ' for €' + euroFmt(cost) + '? Balance €' + euroFmt(balance) + ' → €' + euroFmt(balance - cost) + '.';
|
p.textContent = head + ' for €' + euroFmt(cost) + '? Balance €' + euroFmt(balance) + ' → €' + euroFmt(balance - cost) + '.';
|
||||||
var row = document.createElement('div');
|
var row = document.createElement('div');
|
||||||
row.className = 'mt-2 flex gap-1.5';
|
row.className = 'mt-2 flex gap-1.5';
|
||||||
@@ -926,7 +926,7 @@
|
|||||||
yes.addEventListener('click', function () { boxEl.remove(); placeOrder(btn); });
|
yes.addEventListener('click', function () { boxEl.remove(); placeOrder(btn); });
|
||||||
var no = document.createElement('button');
|
var no = document.createElement('button');
|
||||||
no.type = 'button';
|
no.type = 'button';
|
||||||
no.className = 'rounded-full border border-[color:var(--ink)]/20 text-[color:var(--ink)]/60 px-2.5 py-0.5';
|
no.className = 'rounded-full border border-ink/20 text-muted px-2.5 py-0.5';
|
||||||
no.textContent = 'Cancel';
|
no.textContent = 'Cancel';
|
||||||
no.addEventListener('click', function () { boxEl.remove(); });
|
no.addEventListener('click', function () { boxEl.remove(); });
|
||||||
row.appendChild(yes);
|
row.appendChild(yes);
|
||||||
@@ -954,4 +954,4 @@
|
|||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "scripts"}}<script src="/static/js/adventure-actions.js" defer></script>{{end}}
|
{{define "scripts"}}<script src="{{asset "/static/js/adventure-actions.js"}}" defer></script>{{end}}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"pete/internal/config"
|
"pete/internal/config"
|
||||||
|
"pete/internal/feedwatch"
|
||||||
"pete/internal/ingestion"
|
"pete/internal/ingestion"
|
||||||
"pete/internal/matrix"
|
"pete/internal/matrix"
|
||||||
"pete/internal/poster"
|
"pete/internal/poster"
|
||||||
@@ -243,6 +244,15 @@ func main() {
|
|||||||
// Start pollers
|
// Start pollers
|
||||||
poller := ingestion.NewPoller(cfg.Sources, processItem)
|
poller := ingestion.NewPoller(cfg.Sources, processItem)
|
||||||
poller.Start(ctx)
|
poller.Start(ctx)
|
||||||
|
|
||||||
|
// Tell the owner when a feed stops fetching, comes back, or goes quiet.
|
||||||
|
// Without an admin room there is nowhere to say it, and marking alerts as
|
||||||
|
// sent into the void would mean they never arrive once a room is set.
|
||||||
|
if mx.HasAdminRoom() {
|
||||||
|
go feedwatch.Run(ctx, cfg.Sources, mx.SendAdminNotice)
|
||||||
|
} else {
|
||||||
|
slog.Warn("feed alerts off: set matrix.admin_room to have Pete report broken or quiet feeds")
|
||||||
|
}
|
||||||
slog.Info("pollers started")
|
slog.Info("pollers started")
|
||||||
|
|
||||||
if postingEnabled && roundRobinMode {
|
if postingEnabled && roundRobinMode {
|
||||||
@@ -305,8 +315,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run maintenance on startup
|
// Prune now and then daily. Startup-only meant a service that ran for weeks
|
||||||
storage.RunMaintenance()
|
// between deploys never pruned at all.
|
||||||
|
storage.StartMaintenance(ctx)
|
||||||
|
|
||||||
// Wait for shutdown signal
|
// Wait for shutdown signal
|
||||||
sig := <-sigCh
|
sig := <-sigCh
|
||||||
@@ -423,7 +434,7 @@ func runLocal(cfg *config.Config) {
|
|||||||
ws.StartAdventureAlerts(ctx)
|
ws.StartAdventureAlerts(ctx)
|
||||||
slog.Info("local: web UI listening", "addr", cfg.Web.ListenAddr)
|
slog.Info("local: web UI listening", "addr", cfg.Web.ListenAddr)
|
||||||
|
|
||||||
storage.RunMaintenance()
|
storage.StartMaintenance(ctx)
|
||||||
|
|
||||||
sig := <-sigCh
|
sig := <-sigCh
|
||||||
slog.Info("local: shutdown signal received", "signal", sig)
|
slog.Info("local: shutdown signal received", "signal", sig)
|
||||||
|
|||||||
+16
-1
@@ -1,11 +1,26 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
|
||||||
|
// Pete's palette colours change with the phase (and the casino room), so they
|
||||||
|
// live in CSS vars set on <html> by input.css. Each is exposed as bare rgb
|
||||||
|
// channels so Tailwind can put an opacity on it: text-ink/80, border-ink/10,
|
||||||
|
// bg-accent/20. Writing text-[color:var(--ink)]/80 instead compiles to nothing.
|
||||||
|
const phase = (name) => `rgb(var(--${name}-rgb) / <alpha-value>)`;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: [
|
||||||
"./internal/web/templates/**/*.html",
|
"./internal/web/templates/**/*.html",
|
||||||
"./internal/web/static/**/*.{js,css}",
|
"./internal/web/static/**/*.{js,css}",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
colors: {
|
||||||
|
ink: phase("ink"),
|
||||||
|
muted: phase("muted"),
|
||||||
|
accent: phase("accent"),
|
||||||
|
warn: phase("warn"),
|
||||||
|
card: phase("card"),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user