main
js 113 lines 4.22 KB
Raw
1 import { Controller } from "@hotwired/stimulus"
2
3 const PRESETS = [
4 { label: "Dev", env: "dev", dot: "bg-amber-400" },
5 { label: "Production", env: "production", dot: "bg-emerald-400" },
6 ]
7
8 export default class extends Controller {
9 static targets = ["panel"]
10 static values = { open: Boolean, environment: String, loading: Boolean }
11
12 connect() {
13 this.loadEnvironment()
14 }
15
16 toggle() {
17 this.openValue = !this.openValue
18 this.render()
19 }
20
21 async select(event) {
22 const env = event.currentTarget.dataset.env
23 if (env === this.environmentValue || this.loadingValue) return
24 this.loadingValue = true
25 this.render()
26 try {
27 const res = await fetch("/dev/environment", {
28 method: "PUT",
29 headers: { "Content-Type": "application/json", "X-CSRF-Token": this.csrfToken },
30 body: JSON.stringify({ environment: env }),
31 })
32 const data = await res.json()
33 this.environmentValue = data.environment
34 } catch (_) {}
35 finally {
36 this.loadingValue = false
37 this.render()
38 }
39 }
40
41 async loadEnvironment() {
42 try {
43 const res = await fetch("/dev/environment")
44 const data = await res.json()
45 this.environmentValue = data.environment
46 } catch (_) {
47 this.environmentValue = "dev"
48 }
49 this.render()
50 }
51
52 render() {
53 const panel = this.panelTarget
54 const current = this.environmentValue
55 const loading = this.loadingValue
56
57 if (!this.openValue) {
58 panel.classList.add("hidden")
59 panel.innerHTML = ""
60 return
61 }
62
63 panel.classList.remove("hidden")
64
65 const rows = PRESETS.map((p, i) => {
66 const isActive = p.env === current
67 const isLast = i === PRESETS.length - 1
68 const activeBg = isActive ? "bg-brand-500/10" : "hover:bg-surface-600"
69 const border = isLast ? "" : "border-b border-surface-600"
70 const checkFill = isActive ? "bg-brand-500 border-brand-500" : "border-surface-400"
71 const labelCol = isActive ? "text-brand-400" : "text-gray-200"
72 const check = isActive
73 ? `<svg class="w-2.5 h-2.5 text-white" viewBox="0 0 12 12" fill="currentColor"><path d="M10.28 2.28a.75.75 0 0 0-1.06 0L4.5 7 2.78 5.28a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.06 0l5.25-5.25a.75.75 0 0 0 0-1.06Z"/></svg>`
74 : ""
75 const dot = isActive
76 ? `<span class="ml-auto h-1.5 w-1.5 rounded-full ${p.dot}"></span>`
77 : ""
78 return `<button data-env="${p.env}"
79 data-action="click->dev-panel#select"
80 ${loading ? "disabled" : ""}
81 class="flex w-full items-center gap-3 px-3 py-2.5 text-left transition ${border} ${activeBg} disabled:opacity-50">
82 <span class="flex h-4 w-4 shrink-0 items-center justify-center rounded-full border ${checkFill}">${check}</span>
83 <div class="min-w-0">
84 <p class="text-sm font-medium leading-tight ${labelCol}">${p.label}</p>
85 <p class="mt-0.5 text-[11px] text-gray-500">${p.env}</p>
86 </div>
87 ${dot}
88 </button>`
89 }).join("")
90
91 panel.innerHTML = `
92 <div class="w-52 overflow-hidden rounded border border-surface-500 bg-surface-700 shadow-xl">
93 <div class="flex items-center justify-between px-3 py-2 border-b border-surface-600">
94 <div class="flex items-center gap-2">
95 <span class="rounded bg-brand-500/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-brand-400">dev</span>
96 <span class="text-xs text-gray-400">Auth Target</span>
97 </div>
98 <button data-action="click->dev-panel#toggle"
99 class="rounded p-0.5 text-gray-500 hover:bg-surface-600 hover:text-gray-300 transition-colors">
100 <svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="currentColor">
101 <path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 1 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 1 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 0 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06z"/>
102 </svg>
103 </button>
104 </div>
105 <div>${rows}</div>
106 </div>
107 `
108 }
109
110 get csrfToken() {
111 return document.querySelector("meta[name=csrf-token]")?.content ?? ""
112 }
113 }