Name Generator Rework
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
# NameGen API Documentation
|
||||||
|
|
||||||
|
## Base URL
|
||||||
|
|
||||||
|
```
|
||||||
|
https://namegen.fernandovideira.com/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fantasy Name Endpoints
|
||||||
|
|
||||||
|
| Race/Type | Endpoint | Query Parameters |
|
||||||
|
| ------------ | --------------------------- | ---------------- | ------ | ------- |
|
||||||
|
| Elf | /fantasy/elf-names | gender=male | female | child |
|
||||||
|
| Dwarf | /fantasy/dwarf-names | gender=male | female |
|
||||||
|
| Arakocra | /fantasy/arakocra-names | - |
|
||||||
|
| Aasimar | /fantasy/aasimar-names | gender=male | female |
|
||||||
|
| Bugbear | /fantasy/bugbear-names | - |
|
||||||
|
| Centaur | /fantasy/centaur-names | gender=male | female |
|
||||||
|
| Changeling | /fantasy/changeling-names | - |
|
||||||
|
| Deep Gnome | /fantasy/deep-gnome-names | gender=male | female |
|
||||||
|
| Deva | /fantasy/deva-names | gender=male | female |
|
||||||
|
| Dragonborn | /fantasy/dragonborn-names | gender=male | female | surname |
|
||||||
|
| Drow | /fantasy/drow-names | gender=male | female | surname |
|
||||||
|
| Duergar | /fantasy/duergar-names | gender=male | female |
|
||||||
|
| Eladrin | /fantasy/eladrin-names | gender=male | female |
|
||||||
|
| Fairy | /fantasy/fairy-names | gender=male | female |
|
||||||
|
| Firbolg | /fantasy/firbolg-names | gender=male | female | child |
|
||||||
|
| Genasi | /fantasy/genasi-names | - |
|
||||||
|
| Githyanki | /fantasy/githyanki-names | gender=male | female |
|
||||||
|
| Githzerai | /fantasy/githzerai-names | gender=male | female |
|
||||||
|
| Gnome | /fantasy/gnome-names | gender=male | female |
|
||||||
|
| Goblin | /fantasy/goblin-names | gender=male | female |
|
||||||
|
| Goliath | /fantasy/goliath-names | gender=male | female |
|
||||||
|
| Grung | /fantasy/grung-names | - |
|
||||||
|
| Half-Elf | /fantasy/half-elf-names | gender=male | female |
|
||||||
|
| Half-Orc | /fantasy/half-orc-names | gender=male | female |
|
||||||
|
| Halfling | /fantasy/halfling-names | gender=male | female |
|
||||||
|
| Harengon | /fantasy/harengon-names | gender=male | female |
|
||||||
|
| Hobgoblin | /fantasy/hobgoblin-names | - |
|
||||||
|
| Human | /fantasy/human-names | gender=male | female |
|
||||||
|
| Kalashtar | /fantasy/kalashtar-names | - |
|
||||||
|
| Kenku | /fantasy/kenku-names | - |
|
||||||
|
| Kobold | /fantasy/kobold-names | - |
|
||||||
|
| Leonin | /fantasy/leonin-names | gender=male | female |
|
||||||
|
| Lizardfolk | /fantasy/lizardfolk-names | gender=male | female |
|
||||||
|
| Locathah | /fantasy/locathah-names | - |
|
||||||
|
| Loxodon | /fantasy/loxodon-names | gender=male | female |
|
||||||
|
| Minotaur | /fantasy/minotaur-names | gender=male | female |
|
||||||
|
| Orc | /fantasy/orc-names | gender=male | female |
|
||||||
|
| Satyr | /fantasy/satyr-names | gender=male | female | child |
|
||||||
|
| Shadar-Kai | /fantasy/shadar-kai-names | gender=male | female |
|
||||||
|
| Shardmind | /fantasy/shardmind-names | - |
|
||||||
|
| Shifter | /fantasy/shifter-names | gender=male | female |
|
||||||
|
| Simic Hybrid | /fantasy/simic-hybrid-names | gender=male | female |
|
||||||
|
| Tabaxi | /fantasy/tabaxi-names | gender=male | female |
|
||||||
|
| Tiefling | /fantasy/tiefling-names | gender=male | female | virtue |
|
||||||
|
| Tortle | /fantasy/tortle-names | - |
|
||||||
|
| Triton | /fantasy/triton-names | gender=male | female |
|
||||||
|
| Vedalken | /fantasy/vedalken-names | gender=male | female |
|
||||||
|
| Verdan | /fantasy/verdan-names | - |
|
||||||
|
| Warforged | /fantasy/warforged-names | - |
|
||||||
|
| Wilden | /fantasy/wilden-names | gender=male | female |
|
||||||
|
| Yuan-Ti | /fantasy/yuan-ti-names | - |
|
||||||
|
|
||||||
|
## Modern Name Endpoints
|
||||||
|
|
||||||
|
| Language/Type | Endpoint | Query Parameters |
|
||||||
|
| ---------------------- | ------------------ | ---------------- | ------ | ------ |
|
||||||
|
| Modern English | /modern/english | gender=male | female | unisex |
|
||||||
|
| Modern Portuguese (EU) | /modern/portuguese | gender=male | female | unisex |
|
||||||
|
|
||||||
|
## How to Query the API
|
||||||
|
|
||||||
|
- All endpoints return a JSON object with a `names` array.
|
||||||
|
- Use the `gender` query parameter where supported (e.g., `?gender=female`).
|
||||||
|
- Example request:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET https://namegen.fernandovideira.com/v1/fantasy/elf-names?gender=female
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
GET https://namegen.fernandovideira.com/v1/modern/english?gender=unisex
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"names": [
|
||||||
|
"Aelene Galanodel",
|
||||||
|
"Lia Siannodel",
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
+198
-317
@@ -1,337 +1,218 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="name-generator">
|
<div class="name-generator-layout">
|
||||||
<h2>Name Generator</h2>
|
<div class="page-header">
|
||||||
<div class="controls">
|
<h2>Name Generator</h2>
|
||||||
<div class="name-type-switch">
|
<div class="page-controls">
|
||||||
<span class="switch-label">Name Type:</span>
|
<button
|
||||||
<div class="switch-container">
|
class="button-secondary"
|
||||||
<button
|
@click="addGenerator"
|
||||||
type="button"
|
:disabled="generators.length >= 6"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
<rect
|
<svg
|
||||||
x="9"
|
width="16"
|
||||||
y="9"
|
height="16"
|
||||||
width="13"
|
viewBox="0 0 24 24"
|
||||||
height="13"
|
|
||||||
rx="2"
|
|
||||||
ry="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
fill="none"
|
fill="none"
|
||||||
/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path
|
>
|
||||||
d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
|
<line
|
||||||
stroke="currentColor"
|
x1="12"
|
||||||
stroke-width="2"
|
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"
|
fill="none"
|
||||||
/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</svg>
|
>
|
||||||
</li>
|
<line
|
||||||
</ul>
|
x1="5"
|
||||||
|
y1="12"
|
||||||
|
x2="19"
|
||||||
|
y2="12"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Remove Generator
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Toast notification -->
|
<div class="generators-grid" :class="`grid-${generators.length}`">
|
||||||
<div v-if="showToast" class="toast">
|
<NameGeneratorCard v-for="id in generators" :key="id" :card-id="id" />
|
||||||
{{ toastMessage }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
import NameGeneratorCard from "./NameGeneratorCard.vue";
|
||||||
|
|
||||||
const modern = ref(false);
|
const generators = ref([1, 2, 3, 4]);
|
||||||
const races = [
|
let nextId = 5;
|
||||||
{ 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" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Modern name arrays
|
function addGenerator() {
|
||||||
const modernMaleNames = [
|
if (generators.value.length < 6) {
|
||||||
"James",
|
generators.value.push(nextId++);
|
||||||
"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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyToClipboard(name) {
|
function removeGenerator() {
|
||||||
try {
|
if (generators.value.length > 1) {
|
||||||
await navigator.clipboard.writeText(name);
|
generators.value.pop();
|
||||||
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 showToastMessage(message) {
|
|
||||||
toastMessage.value = message;
|
|
||||||
showToast.value = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
showToast.value = false;
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
</script>
|
</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>
|
||||||
|
|||||||
@@ -0,0 +1,518 @@
|
|||||||
|
<template>
|
||||||
|
<div class="name-generator-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Generator {{ cardId }}</h3>
|
||||||
|
<button class="reset-btn" @click="resetGenerator" title="Reset Generator">
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M21 3v5h-5"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3 21v-5h5"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<div class="name-type-switch">
|
||||||
|
<span class="switch-label">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 v-if="modern">
|
||||||
|
Language:
|
||||||
|
<select v-model="language">
|
||||||
|
<option value="english">English</option>
|
||||||
|
<option value="portuguese">Portuguese (EU)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Gender:
|
||||||
|
<select v-model="gender">
|
||||||
|
<option value="male">Male</option>
|
||||||
|
<option value="female">Female</option>
|
||||||
|
<option v-if="modern" value="unisex">Unisex</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="button-generate" @click="generate" :disabled="isLoading">
|
||||||
|
{{ isLoading ? "Generating..." : "Generate" }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul class="results" v-if="fullNames.length > 0">
|
||||||
|
<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="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="9"
|
||||||
|
y="9"
|
||||||
|
width="13"
|
||||||
|
height="13"
|
||||||
|
rx="2"
|
||||||
|
ry="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
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"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Toast notification -->
|
||||||
|
<div v-if="showToast" class="toast">
|
||||||
|
{{ toastMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
cardId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const modern = ref(false);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
const races = [
|
||||||
|
{ label: "Aasimar", value: "aasimar" },
|
||||||
|
{ label: "Arakocra", value: "arakocra" },
|
||||||
|
{ label: "Bugbear", value: "bugbear" },
|
||||||
|
{ label: "Centaur", value: "centaur" },
|
||||||
|
{ label: "Changeling", value: "changeling" },
|
||||||
|
{ label: "Deep Gnome", value: "deep-gnome" },
|
||||||
|
{ label: "Deva", value: "deva" },
|
||||||
|
{ label: "Dragonborn", value: "dragonborn" },
|
||||||
|
{ label: "Drow", value: "drow" },
|
||||||
|
{ label: "Duergar", value: "duergar" },
|
||||||
|
{ label: "Dwarf", value: "dwarf" },
|
||||||
|
{ label: "Eladrin", value: "eladrin" },
|
||||||
|
{ label: "Elf", value: "elf" },
|
||||||
|
{ label: "Fairy", value: "fairy" },
|
||||||
|
{ label: "Firbolg", value: "firbolg" },
|
||||||
|
{ label: "Genasi", value: "genasi" },
|
||||||
|
{ label: "Githyanki", value: "githyanki" },
|
||||||
|
{ label: "Githzerai", value: "githzerai" },
|
||||||
|
{ label: "Gnome", value: "gnome" },
|
||||||
|
{ label: "Goblin", value: "goblin" },
|
||||||
|
{ label: "Goliath", value: "goliath" },
|
||||||
|
{ label: "Grung", value: "grung" },
|
||||||
|
{ label: "Half-Elf", value: "half-elf" },
|
||||||
|
{ label: "Half-Orc", value: "half-orc" },
|
||||||
|
{ label: "Halfling", value: "halfling" },
|
||||||
|
{ label: "Harengon", value: "harengon" },
|
||||||
|
{ label: "Hobgoblin", value: "hobgoblin" },
|
||||||
|
{ label: "Human", value: "human" },
|
||||||
|
{ label: "Kalashtar", value: "kalashtar" },
|
||||||
|
{ label: "Kenku", value: "kenku" },
|
||||||
|
{ label: "Kobold", value: "kobold" },
|
||||||
|
{ label: "Leonin", value: "leonin" },
|
||||||
|
{ label: "Lizardfolk", value: "lizardfolk" },
|
||||||
|
{ label: "Locathah", value: "locathah" },
|
||||||
|
{ label: "Loxodon", value: "loxodon" },
|
||||||
|
{ label: "Minotaur", value: "minotaur" },
|
||||||
|
{ label: "Orc", value: "orc" },
|
||||||
|
{ label: "Satyr", value: "satyr" },
|
||||||
|
{ label: "Shadar-Kai", value: "shadar-kai" },
|
||||||
|
{ label: "Shardmind", value: "shardmind" },
|
||||||
|
{ label: "Shifter", value: "shifter" },
|
||||||
|
{ label: "Simic Hybrid", value: "simic-hybrid" },
|
||||||
|
{ label: "Tabaxi", value: "tabaxi" },
|
||||||
|
{ label: "Tiefling", value: "tiefling" },
|
||||||
|
{ label: "Tortle", value: "tortle" },
|
||||||
|
{ label: "Triton", value: "triton" },
|
||||||
|
{ label: "Vedalken", value: "vedalken" },
|
||||||
|
{ label: "Verdan", value: "verdan" },
|
||||||
|
{ label: "Warforged", value: "warforged" },
|
||||||
|
{ label: "Wilden", value: "wilden" },
|
||||||
|
{ label: "Yuan-Ti", value: "yuan-ti" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const race = ref(races[0].value);
|
||||||
|
const language = ref("english");
|
||||||
|
const gender = ref("male");
|
||||||
|
const fullNames = ref([]);
|
||||||
|
const showToast = ref(false);
|
||||||
|
const toastMessage = ref("");
|
||||||
|
|
||||||
|
function resetGenerator() {
|
||||||
|
modern.value = false;
|
||||||
|
race.value = races[0].value;
|
||||||
|
language.value = "english";
|
||||||
|
gender.value = "male";
|
||||||
|
fullNames.value = [];
|
||||||
|
showToast.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generate() {
|
||||||
|
fullNames.value = [];
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (modern.value) {
|
||||||
|
// Use API for modern names
|
||||||
|
const url = `https://namegen.fernandovideira.com/v1/modern/${language.value}?gender=${gender.value}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success && data.names) {
|
||||||
|
fullNames.value = data.names;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to fetch modern names:",
|
||||||
|
data.message || "Unknown error"
|
||||||
|
);
|
||||||
|
fullNames.value = ["Error generating names"];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use API for fantasy names
|
||||||
|
const url = `https://namegen.fernandovideira.com/v1/fantasy/${race.value}-names?gender=${gender.value}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success && data.names) {
|
||||||
|
fullNames.value = data.names;
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Failed to fetch fantasy names:",
|
||||||
|
data.message || "Unknown error"
|
||||||
|
);
|
||||||
|
fullNames.value = ["Error generating names"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching names:", error);
|
||||||
|
fullNames.value = [`Error: ${error.message}`];
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 showToastMessage(message) {
|
||||||
|
toastMessage.value = message;
|
||||||
|
showToast.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
showToast.value = false;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.name-generator-card {
|
||||||
|
background: var(--color-card);
|
||||||
|
border: 1px solid var(--color-card-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
min-height: 400px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--color-card-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: var(--spacing-xs);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn:hover {
|
||||||
|
background: var(--color-surface-hover);
|
||||||
|
color: var(--color-text);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-type-switch {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-container {
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid var(--color-card-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-button {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
background: var(--color-background);
|
||||||
|
border: none;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-button:hover {
|
||||||
|
background: var(--color-surface-hover);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-button.active {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-button + .switch-button {
|
||||||
|
border-left: 1px solid var(--color-card-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
border: 1px solid var(--color-card-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--color-background);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 2px var(--color-primary-alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-generate {
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin-top: var(--spacing-sm);
|
||||||
|
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-generate:hover:not(:disabled) {
|
||||||
|
background: #2563eb;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-generate:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-item {
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
background: var(--surface-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-item:hover {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
border-color: var(--border-hover);
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-icon {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-item:hover .copy-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
position: absolute;
|
||||||
|
bottom: var(--spacing-sm);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: var(--success);
|
||||||
|
color: var(--success-text);
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
z-index: 10;
|
||||||
|
animation: slideUp 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-50%) translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user