commit 3ad7aa5514988a78ad4a5f900e324f967597c199 Author: Stephanie Gredell Date: Fri Apr 25 20:40:49 2025 -0700 Initial commit. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2f8b576 --- /dev/null +++ b/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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d3554d3 --- /dev/null +++ b/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= diff --git a/index.html b/index.html new file mode 100644 index 0000000..97a0670 --- /dev/null +++ b/index.html @@ -0,0 +1,75 @@ + + + + + + Coffee Ordering Interface + + + +
+
+

Terminal.shop

+ +
+ +
+ +
+
+
+

Select Coffee

+
+ {{range . }} + + {{end}} +
+
+
+
+ +
+
+
+
+

Current Weight:

+ 0.0g +
+
+

Weight Threshold:

+ +
+
+
+
+
+
+
+ + ⏱️ Orders when low +
+
+ + +
+
+
+ +
+
+ +
+
+
+
+ + + + diff --git a/server.go b/server.go new file mode 100644 index 0000000..e6ea5c4 --- /dev/null +++ b/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) + } +} diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..7253899 --- /dev/null +++ b/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); + } +}) diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..dd6a027 --- /dev/null +++ b/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%); +}