1import { Controller } from "@hotwired/stimulus" 2 3// Year / month / day birthdate picker. 4// 5// Year-first ordering matches how Koreans anchor age ("ëª ë ì?") and stays 6// unambiguous for international buddies, unlike a native date input that 7// renders in the browser's locale format. Three <select>s drive a single 8// hidden user[birthdate] field (YYYY-MM-DD) so the backend contract is 9// unchanged, and a live ë§ ëì´ readout keeps the 18+ gate transparent. 10export default class extends Controller { 11 static targets = ["year", "month", "day", "hidden", "readout"] 12 static values = { 13 minAge: { type: Number, default: 18 }, 14 today: String, // YYYY-MM-DD, injected so age math matches the server clock 15 prompt: String, 16 validTemplate: String, // uses %{age} 17 underage: String, 18 } 19 20 connect() { 21 this.syncDays() 22 this.sync() 23 } 24 25 change() { 26 this.syncDays() 27 this.sync() 28 } 29 30 // Trim day options that don't exist in the chosen month/year (e.g. Feb 30), 31 // clamping a now-invalid selection down to the last real day. 32 syncDays() { 33 const year = Number(this.yearTarget.value) || this.todayParts().year
34 const month = Number(this.monthTarget.value) 35 const max = month ? new Date(year, month, 0).getDate() : 31 36 37 this.dayTarget.querySelectorAll("option[value]").forEach((option) => { 38 if (!option.value) return 39 option.hidden = Number(option.value) > max 40 }) 41 42 if (Number(this.dayTarget.value) > max) this.dayTarget.value = String(max) 43 } 44 45 sync() { 46 const year = this.yearTarget.value 47 const month = this.monthTarget.value 48 const day = this.dayTarget.value 49 50 if (!year || !month || !day) { 51 this.hiddenTarget.value = "" 52 this.paint(this.promptValue, "neutral") 53 return 54 } 55 56 const iso = `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}` 57 this.hiddenTarget.value = iso 58 59 const age = this.ageFrom(Number(year), Number(month), Number(day)) 60 if (age < this.minAgeValue) { 61 this.paint(this.underageValue, "safety") 62 } else { 63 this.paint(this.validTemplateValue.replace("%{age}", age), "confirm") 64 } 65 } 66 67 ageFrom(year, month, day) { 68 const { year: ty, month: tm, day: td } = this.todayParts() 69 let age = ty - year 70 if (tm < month || (tm === month && td < day)) age -= 1 71 return age 72 } 73 74 todayParts() { 75 const [year, month, day] = this.todayValue.split("-").map(Number) 76 return { year, month, day } 77 } 78 79 paint(text, tone) { 80 if (!this.hasReadoutTarget) return 81 this.readoutTarget.textContent = text 82 const colors = { 83 neutral: "var(--kb-caption)", 84 confirm: "var(--kb-ink)", 85 safety: "var(--kb-safety-deep)", 86 } 87 this.readoutTarget.style.color = colors[tone] 88 } 89}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.