import { useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { api, type CurrentUser, type NameTheme } from "../api/client"; import CopyButton from "../components/CopyButton"; import { readableError } from "../utils/errors"; import { generateServerName, generateUsername, generatePassword, passwordEntropyBits, passwordStrengthLabel, type UsernameOptions, type PasswordOptions, } from "../utils/generators"; const MIXED = "mixed"; export default function Generator({ user }: { user: CurrentUser }) { // ─── Server name ─────────────────────────────────────────────────────── const [existingServerNames, setExistingServerNames] = useState([]); const [themes, setThemes] = useState(null); const [themesError, setThemesError] = useState(null); const [theme, setTheme] = useState(MIXED); const [serverName, setServerName] = useState(""); useEffect(() => { api.servers .list() .then((res) => setExistingServerNames(res.servers.map((s) => s.name))) .catch(() => { // Best-effort — collision-avoidance just won't have anything to check against. }); }, []); useEffect(() => { api.generator .themes() .then((res) => setThemes(res.themes)) .catch((err) => setThemesError(readableError(err))); }, []); // "Mixed" is every list together, each name once even if it appears in several. const pool = useMemo(() => { if (!themes) return []; if (theme === MIXED) return [...new Set(themes.flatMap((t) => t.names))]; return themes.find((t) => t.id === theme)?.names ?? []; }, [themes, theme]); function rollServerName() { setServerName(generateServerName(pool, existingServerNames)); } // ─── Username ──────────────────────────────────────────────────────── const [usernameOptions, setUsernameOptions] = useState({ separator: "-", includeNumber: true }); const [username, setUsername] = useState(""); function rollUsername() { setUsername(generateUsername(usernameOptions)); } // ─── Password ──────────────────────────────────────────────────────── const [passwordOptions, setPasswordOptions] = useState({ length: 20, uppercase: true, lowercase: true, numbers: true, symbols: true, excludeAmbiguous: false, }); const [password, setPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); function rollPassword() { setPassword(generatePassword(passwordOptions)); } const noCharsetSelected = !passwordOptions.uppercase && !passwordOptions.lowercase && !passwordOptions.numbers && !passwordOptions.symbols; const entropy = passwordEntropyBits(passwordOptions); const strength = passwordStrengthLabel(entropy); return ( <>

Generator

Usernames and passwords are generated locally in your browser — nothing is sent to or stored on the server. Server names are picked in your browser too, from name lists the server hands out.

Server name

Picks from a list of names, avoiding names already in use. {user.role === "admin" && ( <> {" "} Edit the lists. )}

{themesError &&
{themesError}
}
{themes ? `${pool.length} name${pool.length === 1 ? "" : "s"} to pick from.` : "Loading the lists…"}
{serverName && }

Username

An adjective + animal, optionally with a number — easy to say, hard to collide.

{username && }

Password

setPasswordOptions((o) => ({ ...o, length: Number(e.target.value) }))} />
{( [ ["uppercase", "A–Z"], ["lowercase", "a–z"], ["numbers", "0–9"], ["symbols", "!@#$…"], ] as const ).map(([key, label]) => (
))}
{noCharsetSelected &&
Pick at least one character type.
}
{password && ( <> )}
{password && (
{strength.label} (~{entropy} bits of entropy)
)}
); }