Name Generator Rework

This commit is contained in:
2025-08-01 01:13:33 +01:00
parent af9e88c580
commit 6075472f3d
3 changed files with 812 additions and 317 deletions
+198 -317
View File
@@ -1,337 +1,218 @@
<template>
<div class="name-generator">
<h2>Name Generator</h2>
<div class="controls">
<div class="name-type-switch">
<span class="switch-label">Name Type:</span>
<div class="switch-container">
<button
type="button"
class="switch-button"
:class="{ active: !modern }"
@click="modern = false"
>
Fantasy
</button>
<button
type="button"
class="switch-button button-switch"
:class="{ active: modern }"
@click="modern = true"
>
Modern
</button>
</div>
</div>
<label v-if="!modern">
Race:
<select v-model="race">
<option v-for="r in races" :key="r.value" :value="r.value">
{{ r.label }}
</option>
</select>
</label>
<label>
Gender:
<select v-model="gender">
<option value="male">Male</option>
<option value="female">Female</option>
</select>
</label>
<label>
Count:
<select v-model="count">
<option :value="1">1 name</option>
<option :value="3">3 names</option>
<option :value="5">5 names</option>
<option :value="8">8 names</option>
<option :value="10">10 names</option>
<option :value="15">15 names</option>
<option :value="20">20 names</option>
</select>
</label>
</div>
<button class="button-main" @click="generate">Generate</button>
<ul class="results">
<li
v-for="name in fullNames"
:key="name"
@click="copyToClipboard(name)"
class="name-item"
:title="'Click to copy: ' + name"
>
{{ name }}
<svg
class="copy-icon"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
<div class="name-generator-layout">
<div class="page-header">
<h2>Name Generator</h2>
<div class="page-controls">
<button
class="button-secondary"
@click="addGenerator"
:disabled="generators.length >= 6"
>
<rect
x="9"
y="9"
width="13"
height="13"
rx="2"
ry="2"
stroke="currentColor"
stroke-width="2"
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
/>
<path
d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
stroke="currentColor"
stroke-width="2"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="12"
y1="5"
x2="12"
y2="19"
stroke="currentColor"
stroke-width="2"
/>
<line
x1="5"
y1="12"
x2="19"
y2="12"
stroke="currentColor"
stroke-width="2"
/>
</svg>
Add Generator
</button>
<button
class="button-secondary"
@click="removeGenerator"
:disabled="generators.length <= 1"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
/>
</svg>
</li>
</ul>
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="5"
y1="12"
x2="19"
y2="12"
stroke="currentColor"
stroke-width="2"
/>
</svg>
Remove Generator
</button>
</div>
</div>
<!-- Toast notification -->
<div v-if="showToast" class="toast">
{{ toastMessage }}
<div class="generators-grid" :class="`grid-${generators.length}`">
<NameGeneratorCard v-for="id in generators" :key="id" :card-id="id" />
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import NameGeneratorCard from "./NameGeneratorCard.vue";
const modern = ref(false);
const races = [
{ label: "Dragonborn", value: "dragonborn" },
{ label: "Dwarf", value: "dwarf" },
{ label: "Elf", value: "elf" },
{ label: "Gnome", value: "gnome" },
{ label: "Goblin", value: "goblin" },
{ label: "Half-Elf", value: "half-elf" },
{ label: "Half-Orc", value: "half-orc" },
{ label: "Halfling", value: "halfling" },
{ label: "Human", value: "human" },
{ label: "Orc", value: "orc" },
{ label: "Tiefling", value: "tiefling" },
{ label: "Troll", value: "troll" },
];
const generators = ref([1, 2, 3, 4]);
let nextId = 5;
// Modern name arrays
const modernMaleNames = [
"James",
"John",
"Robert",
"Michael",
"William",
"David",
"Richard",
"Charles",
"Joseph",
"Thomas",
"Christopher",
"Daniel",
"Paul",
"Mark",
"Donald",
"Steven",
"Andrew",
"Kenneth",
"Joshua",
"Kevin",
"Brian",
"George",
"Timothy",
"Ronald",
"Jason",
"Edward",
"Jeffrey",
"Ryan",
"Jacob",
"Gary",
"Nicholas",
"Eric",
"Jonathan",
"Stephen",
"Larry",
"Justin",
"Scott",
"Brandon",
"Benjamin",
"Samuel",
];
const modernFemaleNames = [
"Mary",
"Patricia",
"Jennifer",
"Linda",
"Elizabeth",
"Barbara",
"Susan",
"Jessica",
"Sarah",
"Karen",
"Nancy",
"Lisa",
"Betty",
"Helen",
"Sandra",
"Donna",
"Carol",
"Ruth",
"Sharon",
"Michelle",
"Laura",
"Sarah",
"Kimberly",
"Deborah",
"Dorothy",
"Lisa",
"Nancy",
"Karen",
"Betty",
"Helen",
"Sandra",
"Donna",
"Carol",
"Ruth",
"Sharon",
"Michelle",
"Laura",
"Amy",
"Angela",
"Brenda",
];
const modernLastNames = [
"Smith",
"Johnson",
"Williams",
"Brown",
"Jones",
"Garcia",
"Miller",
"Davis",
"Rodriguez",
"Martinez",
"Hernandez",
"Lopez",
"Gonzalez",
"Wilson",
"Anderson",
"Thomas",
"Taylor",
"Moore",
"Jackson",
"Martin",
"Lee",
"Perez",
"Thompson",
"White",
"Harris",
"Sanchez",
"Clark",
"Ramirez",
"Lewis",
"Robinson",
"Walker",
"Young",
"Allen",
"King",
"Wright",
"Scott",
"Torres",
"Nguyen",
"Hill",
"Flores",
"Green",
"Adams",
"Nelson",
"Baker",
"Hall",
"Rivera",
"Campbell",
"Mitchell",
"Carter",
"Roberts",
];
const race = ref(races[0].value);
const gender = ref("male");
const count = ref(5);
const fullNames = ref([]);
const showToast = ref(false);
const toastMessage = ref("");
function getRandomName(array) {
return array[Math.floor(Math.random() * array.length)];
}
function generateModernNames() {
const names = [];
const firstNames =
gender.value === "male" ? modernMaleNames : modernFemaleNames;
for (let i = 0; i < count.value; i++) {
const firstName = getRandomName(firstNames);
const lastName = getRandomName(modernLastNames);
names.push(`${firstName} ${lastName}`);
}
return names;
}
async function generate() {
fullNames.value = [];
if (modern.value) {
// Generate modern names locally
fullNames.value = generateModernNames();
} else {
// Use API for fantasy names
const selectedRace = race.value;
const firstUrl = `https://names.ironarachne.com/race/${selectedRace}/${gender.value}/${count.value}`;
const familyUrl = `https://names.ironarachne.com/race/${selectedRace}/family/${count.value}`;
try {
const [firstRes, familyRes] = await Promise.all([
fetch(firstUrl),
fetch(familyUrl),
]);
const firstData = await firstRes.json();
const familyData = await familyRes.json();
fullNames.value = firstData.names.map(
(n, i) => `${n} ${familyData.names[i]}`
);
} catch (e) {
console.error("Error fetching names", e);
}
function addGenerator() {
if (generators.value.length < 6) {
generators.value.push(nextId++);
}
}
async function copyToClipboard(name) {
try {
await navigator.clipboard.writeText(name);
showToastMessage(`"${name}" copied to clipboard!`);
} catch (err) {
console.error("Failed to copy to clipboard:", err);
// Fallback for older browsers
const textArea = document.createElement("textarea");
textArea.value = name;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
showToastMessage(`"${name}" copied to clipboard!`);
function removeGenerator() {
if (generators.value.length > 1) {
generators.value.pop();
}
}
function showToastMessage(message) {
toastMessage.value = message;
showToast.value = true;
setTimeout(() => {
showToast.value = false;
}, 2000);
}
</script>
<style scoped src="./NameGenerator.css"></style>
<style scoped>
.name-generator-layout {
padding: var(--spacing-lg);
max-width: 1400px;
margin: 0 auto;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
padding-bottom: var(--spacing-md);
border-bottom: 1px solid var(--border);
}
.page-header h2 {
margin: 0;
color: var(--text-primary);
font-size: 2rem;
}
.page-controls {
display: flex;
gap: var(--spacing-sm);
}
.button-secondary {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--surface);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: var(--border-radius);
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
.button-secondary:hover:not(:disabled) {
background: var(--surface-hover);
border-color: var(--border-hover);
transform: translateY(-1px);
}
.button-secondary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.generators-grid {
display: grid;
gap: var(--spacing-lg);
width: 100%;
}
.grid-1 {
grid-template-columns: 1fr;
max-width: 600px;
margin: 0 auto;
}
.grid-2 {
grid-template-columns: repeat(2, 1fr);
}
.grid-3 {
grid-template-columns: repeat(3, 1fr);
}
.grid-4 {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
}
.grid-5 {
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
}
.grid-6 {
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
}
/* Responsive design */
@media (max-width: 1024px) {
.grid-3,
.grid-4,
.grid-5,
.grid-6 {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: auto;
}
}
@media (max-width: 768px) {
.name-generator-layout {
padding: var(--spacing-md);
}
.page-header {
flex-direction: column;
gap: var(--spacing-md);
align-items: stretch;
}
.page-controls {
justify-content: center;
}
.generators-grid {
grid-template-columns: 1fr !important;
grid-template-rows: auto !important;
}
}
@media (max-width: 480px) {
.page-controls {
flex-direction: column;
}
.button-secondary {
justify-content: center;
}
}
</style>