216 lines
7.4 KiB
Plaintext
216 lines
7.4 KiB
Plaintext
---
|
|
import { useT } from "../../i18n/translations";
|
|
const isRu = Astro.url.pathname.startsWith("/ru");
|
|
const T = useT(isRu ? "ru" : "en");
|
|
---
|
|
|
|
<div id="password-generator" class="mt-8">
|
|
<div class="rounded-xl bg-zinc-900/50 border border-zinc-800/60 p-5">
|
|
<div class="flex flex-col gap-5">
|
|
<div>
|
|
<div class="flex items-center justify-between mb-1">
|
|
<label for="pg-length" class="text-sm font-medium text-zinc-400"
|
|
>{T.length}</label
|
|
>
|
|
<span
|
|
id="pg-length-val"
|
|
class="text-sm font-semibold tabular-nums text-zinc-100">16</span
|
|
>
|
|
</div>
|
|
<input
|
|
id="pg-length"
|
|
type="range"
|
|
min="4"
|
|
max="64"
|
|
value="16"
|
|
class="w-full accent-accent cursor-pointer"
|
|
/>
|
|
<div class="flex justify-between text-xs text-zinc-600 mt-1 select-none">
|
|
<span>4</span><span>64</span>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
{
|
|
[
|
|
{ id: "pg-upper", label: T.uppercase },
|
|
{ id: "pg-lower", label: T.lowercase },
|
|
{ id: "pg-digits", label: T.digits },
|
|
{ id: "pg-symbols", label: T.symbols },
|
|
].map(({ id, label }) => (
|
|
<label class="flex items-center gap-2.5 cursor-pointer select-none group">
|
|
<input
|
|
id={id}
|
|
type="checkbox"
|
|
checked
|
|
class="w-4 h-4 rounded accent-accent cursor-pointer"
|
|
/>
|
|
<span class="text-sm text-zinc-400 group-hover:text-zinc-200 transition-colors">
|
|
{label}
|
|
</span>
|
|
</label>
|
|
))
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p
|
|
id="pg-error"
|
|
role="alert"
|
|
aria-live="polite"
|
|
class="mt-4 text-sm text-red-500 hidden"
|
|
>
|
|
</p>
|
|
|
|
<div class="my-6 rounded-2xl bg-zinc-900/80 border border-zinc-800/80 p-8 h-52 overflow-hidden flex flex-col items-center justify-center gap-3">
|
|
<button
|
|
id="pg-copy-btn"
|
|
type="button"
|
|
class="group relative invisible cursor-copy rounded-xl px-3 py-2 -mx-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950"
|
|
>
|
|
<span
|
|
id="pg-result"
|
|
class="font-mono text-2xl sm:text-3xl font-bold tracking-wide text-zinc-100 break-all select-none group-hover:text-zinc-300"
|
|
style="transition: opacity 0.08s ease, color 0.15s ease;"></span>
|
|
<span
|
|
id="pg-copy-icon"
|
|
class="absolute -right-7 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-zinc-500"
|
|
aria-hidden="true"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="18"
|
|
height="18"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path
|
|
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
|
></path></svg
|
|
>
|
|
</span>
|
|
<span
|
|
id="pg-check-icon"
|
|
class="absolute -right-7 top-1/2 -translate-y-1/2 hidden text-accent"
|
|
aria-hidden="true"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="18"
|
|
height="18"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2.5"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"><path d="M20 6 9 17l-5-5"></path></svg
|
|
>
|
|
</span>
|
|
</button>
|
|
<span
|
|
id="pg-copied-label"
|
|
class="text-xs font-medium text-accent opacity-0 transition-opacity duration-200"
|
|
aria-live="polite"
|
|
aria-atomic="true">{T.copied}</span
|
|
>
|
|
</div>
|
|
|
|
<div class="flex justify-center">
|
|
<button
|
|
id="pg-btn"
|
|
type="button"
|
|
class="w-full sm:w-auto px-8 py-3 bg-accent text-white font-semibold rounded-xl shadow-lg shadow-accent/25 hover:bg-accent-hover active:bg-accent-active focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 transition-colors duration-100 cursor-pointer"
|
|
>
|
|
{T.generate}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
import { CopyFeedback } from "@/lib/client/clipboard";
|
|
import { createErrorDisplay } from "@/lib/client/validation";
|
|
|
|
const isRu = document.documentElement.lang === "ru";
|
|
const ERR_SELECT_TYPE = isRu
|
|
? "Выберите хотя бы один тип символов."
|
|
: "Select at least one character type.";
|
|
|
|
const lengthInput = document.getElementById("pg-length") as HTMLInputElement;
|
|
const lengthVal = document.getElementById("pg-length-val") as HTMLSpanElement;
|
|
const upperCb = document.getElementById("pg-upper") as HTMLInputElement;
|
|
const lowerCb = document.getElementById("pg-lower") as HTMLInputElement;
|
|
const digitsCb = document.getElementById("pg-digits") as HTMLInputElement;
|
|
const symbolsCb = document.getElementById("pg-symbols") as HTMLInputElement;
|
|
const btn = document.getElementById("pg-btn") as HTMLButtonElement;
|
|
const copyBtn = document.getElementById("pg-copy-btn") as HTMLButtonElement;
|
|
const resultEl = document.getElementById("pg-result") as HTMLSpanElement;
|
|
const errorEl = document.getElementById("pg-error") as HTMLParagraphElement;
|
|
const copyIcon = document.getElementById("pg-copy-icon") as HTMLSpanElement;
|
|
const checkIcon = document.getElementById("pg-check-icon") as HTMLSpanElement;
|
|
const copiedLabel = document.getElementById(
|
|
"pg-copied-label",
|
|
) as HTMLSpanElement;
|
|
|
|
const UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
const LOWER = "abcdefghijklmnopqrstuvwxyz";
|
|
const DIGITS = "0123456789";
|
|
const SYMBOLS = "!@#$%^&*()-_=+[]{}|;:,.<>?";
|
|
|
|
lengthInput.addEventListener("input", () => {
|
|
lengthVal.textContent = lengthInput.value;
|
|
});
|
|
|
|
const errors = createErrorDisplay(errorEl);
|
|
const clipboard = new CopyFeedback(copyIcon, checkIcon);
|
|
|
|
function generate() {
|
|
const length = parseInt(lengthInput.value, 10);
|
|
const pools: string[] = [];
|
|
if (upperCb.checked) pools.push(UPPER);
|
|
if (lowerCb.checked) pools.push(LOWER);
|
|
if (digitsCb.checked) pools.push(DIGITS);
|
|
if (symbolsCb.checked) pools.push(SYMBOLS);
|
|
if (pools.length === 0) {
|
|
errors.show(ERR_SELECT_TYPE);
|
|
return;
|
|
}
|
|
errors.clear();
|
|
const required = pools.map((p) => p[Math.floor(Math.random() * p.length)]);
|
|
const combined = pools.join("");
|
|
const rest = Array.from(
|
|
{ length: length - required.length },
|
|
() => combined[Math.floor(Math.random() * combined.length)],
|
|
);
|
|
const chars = [...required, ...rest];
|
|
for (let i = chars.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[chars[i], chars[j]] = [chars[j], chars[i]];
|
|
}
|
|
resultEl.textContent = chars.join("");
|
|
resultEl.style.opacity = "0.5";
|
|
requestAnimationFrame(() =>
|
|
requestAnimationFrame(() => {
|
|
resultEl.style.opacity = "1";
|
|
}),
|
|
);
|
|
copyBtn.classList.remove("invisible");
|
|
}
|
|
|
|
async function copyPassword() {
|
|
const value = resultEl.textContent?.trim();
|
|
if (!value) return;
|
|
await navigator.clipboard.writeText(value);
|
|
clipboard.showCopied();
|
|
copiedLabel.style.opacity = "1";
|
|
setTimeout(() => {
|
|
copiedLabel.style.opacity = "0";
|
|
}, 1500);
|
|
}
|
|
|
|
btn.addEventListener("click", generate);
|
|
copyBtn.addEventListener("click", copyPassword);
|
|
</script>
|