Skip to content

Commit 6ea96df

Browse files
author
Andrew Marcuse
committed
Haikunator names
1 parent 3d76f2f commit 6ea96df

File tree

6 files changed

+273
-2
lines changed

6 files changed

+273
-2
lines changed

haikunator.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Haikunator</title>
8+
</head>
9+
<body>
10+
<div id="app"></div>
11+
<script type="module" src="/src/haikunator.ts"></script>
12+
</body>
13+
</html>

package-lock.json

Lines changed: 76 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"vite": "^7.3.1"
1818
},
1919
"dependencies": {
20-
"any-ascii": "^0.3.3"
20+
"any-ascii": "^0.3.3",
21+
"haikunator": "^2.1.2"
2122
}
2223
}

src/haikunator.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import './style.css'
2+
import Haikunator from 'haikunator'
3+
import { renderHeader } from './components/header.ts'
4+
5+
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
6+
<main class="min-h-screen bg-base-200" data-theme="light">
7+
<section class="mx-auto flex max-w-5xl flex-col gap-8 px-4 py-8 md:px-8 md:py-12">
8+
${renderHeader()}
9+
10+
<section class="rounded-box border border-base-300 bg-base-100 p-8 shadow-sm">
11+
<h1 class="text-3xl font-bold">Haikunator</h1>
12+
<p class="mt-3 text-base-content/70">Generate Heroku-style random names with live options.</p>
13+
14+
<form class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2" action="#" method="post" onsubmit="return false;">
15+
<label class="form-control w-full">
16+
<div class="label">
17+
<span class="label-text">Delimiter</span>
18+
</div>
19+
<input id="delimiter" type="text" value="-" class="input input-bordered w-full" />
20+
</label>
21+
22+
<label class="form-control w-full">
23+
<div class="label">
24+
<span class="label-text">Token length</span>
25+
</div>
26+
<input id="token-length" type="number" min="0" step="1" value="4" class="input input-bordered w-full" />
27+
</label>
28+
29+
<label class="form-control w-full">
30+
<div class="label">
31+
<span class="label-text">Token characters</span>
32+
</div>
33+
<input id="token-chars" type="text" value="0123456789" class="input input-bordered w-full" />
34+
</label>
35+
36+
<label class="form-control w-full">
37+
<div class="label">
38+
<span class="label-text">Number of names</span>
39+
</div>
40+
<input id="name-count" type="number" min="1" step="1" value="10" class="input input-bordered w-full" />
41+
</label>
42+
43+
<label class="form-control w-full md:col-span-2">
44+
<div class="label">
45+
<span class="label-text">Generated names</span>
46+
</div>
47+
<div id="output-list" class="w-full rounded-box border border-base-300 bg-base-200 p-3"></div>
48+
</label>
49+
50+
<div class="md:col-span-2">
51+
<button id="copy-all-button" type="button" class="btn btn-sm">Copy all to clipboard</button>
52+
</div>
53+
</form>
54+
</section>
55+
</section>
56+
</main>
57+
`
58+
59+
const delimiterInput = document.querySelector<HTMLInputElement>('#delimiter')
60+
const tokenLengthInput = document.querySelector<HTMLInputElement>('#token-length')
61+
const tokenCharsInput = document.querySelector<HTMLInputElement>('#token-chars')
62+
const nameCountInput = document.querySelector<HTMLInputElement>('#name-count')
63+
const outputList = document.querySelector<HTMLDivElement>('#output-list')
64+
const copyAllButton = document.querySelector<HTMLButtonElement>('#copy-all-button')
65+
let generatedNames: string[] = []
66+
67+
const copyIconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"></rect><path d="M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"></path><path d="M16 4h2a2 2 0 0 1 2 2v4"></path><path d="M21 14H11"></path><path d="m15 10-4 4 4 4"></path></svg>'
68+
69+
const escapeHtml = (value: string): string =>
70+
value
71+
.replaceAll('&', '&amp;')
72+
.replaceAll('<', '&lt;')
73+
.replaceAll('>', '&gt;')
74+
.replaceAll('"', '&quot;')
75+
.replaceAll("'", '&#39;')
76+
77+
const clampInteger = (value: number, min: number): number => {
78+
if (!Number.isFinite(value)) {
79+
return min
80+
}
81+
82+
return Math.max(min, Math.floor(value))
83+
}
84+
85+
const renderNames = () => {
86+
if (!outputList) {
87+
return
88+
}
89+
90+
outputList.innerHTML = generatedNames
91+
.map(
92+
(name, index) =>
93+
`<div class="mb-2 flex items-center gap-2 last:mb-0"><button type="button" class="btn btn-xs" data-copy-index="${index}" title="Copy name" aria-label="Copy ${escapeHtml(name)}"><span class="block h-4 w-4">${copyIconSvg}</span></button><span class="font-mono">${escapeHtml(name)}</span></div>`,
94+
)
95+
.join('')
96+
}
97+
98+
const generateNames = () => {
99+
if (!delimiterInput || !tokenLengthInput || !tokenCharsInput || !nameCountInput) {
100+
return
101+
}
102+
103+
const delimiter = delimiterInput.value
104+
const tokenLength = clampInteger(Number(tokenLengthInput.value), 0)
105+
const nameCount = clampInteger(Number(nameCountInput.value), 1)
106+
const tokenChars = tokenCharsInput.value || '0123456789'
107+
108+
const haikunator = new Haikunator()
109+
const names: string[] = []
110+
111+
for (let index = 0; index < nameCount; index += 1) {
112+
names.push(
113+
haikunator.haikunate({
114+
delimiter,
115+
tokenLength,
116+
tokenChars,
117+
}),
118+
)
119+
}
120+
121+
generatedNames = names
122+
renderNames()
123+
}
124+
125+
const inputs = [delimiterInput, tokenLengthInput, tokenCharsInput, nameCountInput]
126+
for (const input of inputs) {
127+
input?.addEventListener('input', generateNames)
128+
}
129+
130+
outputList?.addEventListener('click', async (event) => {
131+
const target = event.target
132+
if (!(target instanceof Element)) {
133+
return
134+
}
135+
136+
const button = target.closest<HTMLButtonElement>('button[data-copy-index]')
137+
if (!button) {
138+
return
139+
}
140+
141+
const index = Number(button.dataset.copyIndex)
142+
const value = generatedNames[index]
143+
144+
if (!value) {
145+
return
146+
}
147+
148+
try {
149+
await navigator.clipboard.writeText(value)
150+
button.textContent = '✅'
151+
} catch {
152+
button.textContent = '❌'
153+
}
154+
155+
window.setTimeout(() => {
156+
button.innerHTML = `<span class="block h-4 w-4">${copyIconSvg}</span>`
157+
}, 1000)
158+
})
159+
160+
copyAllButton?.addEventListener('click', async () => {
161+
if (generatedNames.length === 0) {
162+
return
163+
}
164+
165+
try {
166+
await navigator.clipboard.writeText(generatedNames.join('\n'))
167+
copyAllButton.textContent = 'Copied!'
168+
} catch {
169+
copyAllButton.textContent = 'Copy failed'
170+
}
171+
172+
window.setTimeout(() => {
173+
copyAllButton.textContent = 'Copy all to clipboard'
174+
}, 1500)
175+
})
176+
177+
generateNames()

src/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
1717
<a href="/bytecount.html" class="btn btn-primary">Byte Count</a>
1818
<div class="ps-2">count which bytes are in a file</div>
1919
</div>
20+
<div class="flex flex-row items-center">
21+
<a href="/haikunator.html" class="btn btn-primary">Haikunator</a>
22+
<div class="ps-2">generate random Heroku-style names</div>
23+
</div>
2024
<div class="flex flex-row items-center">
2125
<a href="/runecount.html" class="btn btn-primary">Rune Count</a>
2226
<div class="ps-2">count which characters are in a file</div>

vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default defineConfig({
1414
upsideDown: resolve(__dirname, 'upside-down.html'),
1515
urlencode: resolve(__dirname, 'urlencode.html'),
1616
strings: resolve(__dirname, 'strings.html'),
17+
haikunator: resolve(__dirname, 'haikunator.html'),
1718
},
1819
},
1920
},

0 commit comments

Comments
 (0)