Browse Source

Initial commit.

master
Stephanie Gredell 9 months ago
commit
3ad7aa5514
  1. 13
      go.mod
  2. 14
      go.sum
  3. 75
      index.html
  4. 115
      server.go
  5. 75
      static/script.js
  6. 248
      static/styles.css

13
go.mod

@ -0,0 +1,13 @@
module terminalscale
go 1.22.2
require github.com/terminaldotshop/terminal-sdk-go v1.10.0
require (
github.com/gorilla/websocket v1.5.3 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
)

14
go.sum

@ -0,0 +1,14 @@
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/terminaldotshop/terminal-sdk-go v1.10.0 h1:AHmD9ifNd5vJPKYntuvVbxw4OqnSJbgq1FLeEXDQwTY=
github.com/terminaldotshop/terminal-sdk-go v1.10.0/go.mod h1:28WE2YTqRVLNDpZFiPm4ny3f1hjJuJkf0e2Jwy5Gxys=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=

75
index.html

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Coffee Ordering Interface</title>
<link rel="stylesheet" href="/static/styles.css">
</head>
<body>
<div class="coffee-interface">
<header>
<h1><span class="icon"></span> Terminal.shop</h1>
<button id="settings-toggle" class="settings-toggle" title="Settings">
<svg class="settings-icon" viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none">
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"></path>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1Z"></path>
</svg>
</button>
</header>
<div class="content">
<!-- Left column -->
<div class="column">
<div class="card">
<div class="card-content">
<h2>Select Coffee</h2>
<div class="coffee-grid">
{{range . }}
<button class="coffee-button" data-id={{.Name}} data-name={{.Name}} data-color={{.Color}} data-variant-id={{.VariantID}}>
<span class="coffee-name">{{.Name}}</span>
</button>
{{end}}
</div>
</div>
</div>
</div>
<!-- Right column -->
<div class="column">
<div class="card">
<div class="card-content">
<div class="weight-container">
<h2>Current Weight:</h2>
<span class="weight-badge" id="weight-display">0.0g</span>
</div>
<div class="weight-container">
<h2>Weight Threshold:</h2>
<input class="weight-threshold" id="weight-threshold" type="text" />
</div>
</div>
</div>
<div class="card card-auto-order">
<div class="card-content">
<div class="auto-order">
<div>
<label for="auto-order-switch" class="label">Auto Order</label>
<span class="sublabel"><span class="small-icon"></span> Orders when low</span>
</div>
<div class="switch-container">
<input type="checkbox" id="auto-order-switch" class="switch-input">
<label for="auto-order-switch" class="switch"></label>
</div>
</div>
<div>
<button class="order-now">Order Now</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/static/script.js"></script>
</body>
</html>

115
server.go

@ -0,0 +1,115 @@
package main
import (
"context"
"fmt"
"github.com/gorilla/websocket"
"github.com/terminaldotshop/terminal-sdk-go"
"github.com/terminaldotshop/terminal-sdk-go/option"
"log"
"net/http"
"text/template"
)
type ProductView struct {
Name string
Description string
Color string
VariantID string
}
type VariantView struct {
Name string
PriceFormatted string
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func main() {
fs := http.FileServer(http.Dir("static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
http.HandleFunc("/products", getProducts)
http.HandleFunc("/ws", ws)
client := terminal.NewClient(
option.WithBearerToken("trm_test_3532f9f1592e704eadbc"), // defaults to os.LookupEnv("TERMINAL_BEARER_TOKEN")
option.WithEnvironmentDev(),
)
response, err := client.Address.List(context.TODO())
if err != nil {
panic(err.Error())
}
fmt.Printf("%+v\n", response.Data)
log.Println("✅ Server listening on http://localhost:8080/products")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func ws(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
fmt.Println("Upgrade error:", err)
return
}
defer conn.Close()
for {
_, msg, err := conn.ReadMessage()
if err != nil {
fmt.Println("Read Errors", err)
break
}
fmt.Printf("Received", msg)
err = conn.WriteMessage(websocket.TextMessage, []byte("Server got: "+string(msg)))
if err != nil {
fmt.Println("Write error", err)
break
}
}
}
func getProducts(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles("index.html"))
client := terminal.NewClient(
option.WithBearerToken("trm_test_3532f9f1592e704eadbc"), // defaults to os.LookupEnv("TERMINAL_BEARER_TOKEN")
option.WithEnvironmentDev(), // defaults to option.WithEnvironmentProduction()
)
products, err := client.Product.List(context.TODO())
if err != nil {
http.Error(w, "Failed to fetch products", http.StatusInternalServerError)
return
}
views := []ProductView{}
for _, p := range products.Data {
if !p.Tags.MarketNa || p.Subscription == "required" {
continue
}
variantId := ""
if len(p.Variants) > 0 {
variantId = p.Variants[0].ID
}
views = append(views, ProductView{
Name: p.Name,
Description: p.Description,
Color: p.Tags.Color,
VariantID: variantId,
})
}
w.Header().Set("Content-Type", "text/html")
if err := tmpl.Execute(w, views); err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
}
}

75
static/script.js

@ -0,0 +1,75 @@
document.addEventListener("DOMContentLoaded", () => {
// State
let selectedCoffee = {
id: "espresso",
name: "Espresso",
icon: "☕",
}
let weight = 0
let autoOrder = false
// DOM Elements
const coffeeButtons = document.querySelectorAll(".coffee-button")
const weightDisplay = document.getElementById("weight-display")
const autoOrderSwitch = document.getElementById("auto-order-switch")
const activeCoffee = document.querySelector('.coffee-button.active')
const orderNow = document.querySelector('.order-now')
if (activeCoffee === null) {
orderNow.disabled = true
}
// Coffee Selection
coffeeButtons.forEach((button) => {
button.addEventListener("click", function () {
coffeeButtons.forEach((btn) => {
btn.classList.remove("active")
btn.style.backgroundColor = ""
})
// Remove active class from all buttons
coffeeButtons.forEach((btn) => btn.classList.remove("active"))
const color = this.dataset.color
// Add active class to clicked button
this.classList.add("active")
orderNow.disabled = false
this.style.backgroundColor = color
// Update selected coffee
selectedCoffee = {
id: this.dataset.id,
name: this.dataset.name,
icon: this.dataset.icon,
}
})
})
// Auto Order Toggle
autoOrderSwitch.addEventListener("change", function (event) {
const hasActiveCoffee = document.querySelector('.coffee-button.active');
if (!hasActiveCoffee) {
event.preventDefault();
this.checked = !this.checked;
return;
}
autoOrder = this.checked
document.querySelector('.order-now').disabled = this.checked;
console.log(this.checked)
})
// Simulate weight changes
weight += 1
weightDisplay.textContent =weight + "oz"
const socket = new WebSocket("ws://localhost/ws");
socket.onOpen = () => {
console.log('Connected to server');
}
socket.onMessage = (event) => {
console.log(event.data);
}
})

248
static/styles.css

@ -0,0 +1,248 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans",
"Helvetica Neue", sans-serif;
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #000000;
}
.coffee-interface {
width: 500px;
height: 320px;
background-color: #444;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
/* Fixed width to prevent any resizing */
min-width: 500px;
max-width: 500px;
}
header {
background-color: rgb(255,94,0);
color: white;
padding: 8px 16px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
height: 40px; /* Fixed height for header */
display: flex;
}
h1 {
font-size: 1.125rem;
font-weight: 700;
display: flex;
align-items: center;
}
.settings-toggle {
background: none;
border: none;
color: white;
cursor: pointer;
padding: 5px;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border-radius: 50%;
transition: background-color 0.2s;
margin-left: auto;
}
.settings-toggle:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.settings-icon {
color: white;
}
.icon {
margin-right: 8px;
}
.content {
display: flex;
flex: 1;
gap: 8px;
padding: 8px;
height: 280px; /* Fixed height for content area */
}
.column {
width: 242px; /* Exact half of 500px minus padding and gap */
display: flex;
flex-direction: column;
gap: 8px;
}
.card {
background-color: white;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
flex: 1;
}
/* Make the auto-order card take up more space */
.card-auto-order {
flex: 2;
}
.card-content {
padding: 12px;
height: 100%;
}
h2 {
font-size: 0.9rem;
font-weight: 500;
margin-bottom: 4px;
}
.coffee-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 8px;
}
.coffee-button {
height: 56px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4px;
border: 1px solid #e2e8f0;
border-radius: 6px;
background-color: white;
cursor: pointer;
transition: all 0.2s;
/* Fixed width for buttons */
width: 100px;
}
.coffee-button:hover {
background-color: #f8fafc;
}
.coffee-button.active {
color: white;
border-color: #000000;
}
.coffee-icon {
font-size: 1.25rem;
}
.coffee-name {
font-size: 0.75rem;
font-weight: 500;
}
.weight-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.weight-badge {
font-size: 1.125rem;
font-weight: 700;
padding: 2px 8px;
border: 1px solid #e2e8f0;
border-radius: 4px;
/* Fixed width for weight display to prevent movement */
min-width: 70px;
text-align: center;
}
.weight-threshold {
font-size: 1.125rem;
font-weight: 700;
padding: 2px 8px;
border: 1px solid #e2e8f0;
border-radius: 4px;
/* Fixed width for weight display to prevent movement */
min-width: 70px;
text-align: center;
width: 70px;
}
.auto-order {
display: flex;
justify-content: space-between;
align-items: center;
}
.label {
font-size: 0.9rem;
font-weight: 500;
}
.sublabel {
font-size: 0.75rem;
color: #64748b;
display: flex;
align-items: center;
}
.small-icon {
font-size: 0.75rem;
margin-right: 4px;
}
.switch-container {
position: relative;
}
.switch-input {
height: 0;
width: 0;
visibility: hidden;
position: absolute;
}
.switch {
cursor: pointer;
width: 40px;
height: 20px;
background: #e2e8f0;
display: block;
border-radius: 100px;
position: relative;
}
.switch:after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background: white;
border-radius: 50%;
transition: 0.3s;
}
.switch-input:checked + .switch {
background: rgb(255, 94, 0);
}
.switch-input:checked + .switch:after {
left: calc(100% - 2px);
transform: translateX(-100%);
}
Loading…
Cancel
Save