| 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 | } |