chore: update dependencies and add new features

- Added Font Awesome and Annyang for enhanced UI and voice recognition capabilities.
- Updated package.json to include new dependencies: @fortawesome/fontawesome-free, annyang, and wrangler.
- Modified postcss.config.js for proper syntax.
- Updated style.css to include Font Awesome styles and added new styles for voice search button and footer.
- Adjusted tailwind.config.js to scan all relevant files for dynamic classes.
- Added VSCode settings to ignore unknown at-rules in CSS, SCSS, and LESS.
- Created a Caddyfile for server configuration with basic settings.
This commit is contained in:
Peter Wood
2025-04-22 17:31:47 -04:00
parent 00add8ae3b
commit 0f6cfe3d6c
10 changed files with 1148 additions and 82 deletions

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"css.lint.unknownAtRules": "ignore",
"scss.lint.unknownAtRules": "ignore",
"less.lint.unknownAtRules": "ignore"
}

8
Caddyfile Normal file
View File

@@ -0,0 +1,8 @@
{
auto_https off
}
:80 {
root * /srv
file_server
}

View File

@@ -1,11 +1,10 @@
# ---- Build Stage ---- # ---- Build Stage ----
FROM node:20-alpine AS build FROM node:23-slim AS build
WORKDIR /app WORKDIR /app
# Copy package files and install dependencies # Copy package files and install dependencies
# Copy package.json AND package-lock.json (if available) COPY package.json package-lock.json ./
COPY package*.json ./
RUN npm install RUN npm install
# Copy the rest of the application code # Copy the rest of the application code
@@ -15,16 +14,12 @@ COPY . .
RUN npm run build RUN npm run build
# ---- Serve Stage ---- # ---- Serve Stage ----
FROM nginx:stable-alpine FROM caddy:2.10.0-alpine
WORKDIR /srv
# Copy built assets from the build stage # Copy built assets from the build stage
COPY --from=build /app/dist /usr/share/nginx/html COPY --from=build /app/dist /srv
# Optional: Copy a custom Nginx configuration if needed # Copy Caddy configuration file
# COPY nginx.conf /etc/nginx/conf.d/default.conf COPY Caddyfile /etc/caddy/Caddyfile
# Expose port 80
EXPOSE 80
# Start Nginx in the foreground
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -19,31 +19,22 @@
<div class="flex flex-col sm:flex-row gap-4 mb-6"> <div class="flex flex-col sm:flex-row gap-4 mb-6">
<!-- Wrap input and voice button --> <!-- Wrap input and voice button -->
<div class="relative flex-grow"> <div class="relative flex-grow">
<!-- Search Icon -->
<i
class="fas fa-search absolute inset-y-0 left-3 flex items-center text-gray-500"
></i>
<input <input
type="text" type="text"
id="pokemonName" id="pokemonName"
placeholder="Enter Pokémon name or use mic" placeholder="Enter Pokémon name"
class="w-full px-4 py-2 pr-10 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-200" class="w-full pl-10 pr-10 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 transition duration-200"
/> />
<!-- Microphone Button --> <!-- Microphone Icon -->
<button <button
id="voiceSearchButton" id="voiceSearchButton"
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-500 hover:text-blue-600 focus:outline-none" class="absolute inset-y-0 right-3 flex items-center text-gray-500 hover:text-blue-600 focus:outline-none"
> >
<svg <i class="fas fa-microphone"></i>
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 1 1-6 0v8.25a3 3 0 0 1-3 3Z"
/>
</svg>
</button> </button>
</div> </div>
<button <button
@@ -53,21 +44,44 @@
Search Search
</button> </button>
</div> </div>
<div id="pokemonInfo" class="mt-6 text-center text-gray-700"> <div id="pokemonInfo" class="mt-6 text-center text-gray-700"></div>
<!-- Pokémon info will be displayed here -->
</div>
<!-- History Section - Removed h3 and border --> <!-- History Section -->
<div id="searchHistory" class="mt-8 pt-4"> <div id="searchHistory" class="mt-8 pt-4">
<ul id="historyList" class="list-none space-y-4"> <ul id="historyList" class="list-none space-y-4">
<!-- History items (full cards) will be added here --> <li class="italic text-sm text-center text-gray-600"></li>
<li class="italic text-sm text-center text-gray-600">
No history yet.
</li>
</ul> </ul>
</div> </div>
</div> </div>
<!-- Voice Overlay -->
<div
id="voiceOverlay"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden"
>
<i class="fas fa-microphone text-white text-6xl"></i>
</div>
<!-- Footer -->
<footer
class="mt-8 text-center text-gray-600 flex items-center justify-center gap-4"
>
<p>Made with ❤️ by Daddy.</p>
<a
href="https://github.com/acedanger/pokemon"
target="_blank"
class="text-blue-600 hover:text-blue-800"
>
<i class="fab fa-github w-6 h-6"></i>
</a>
<button
id="themeToggleButton"
class="flex items-center justify-center text-gray-600 hover:text-gray-800 focus:outline-none"
>
<i id="themeIcon" class="fas fa-moon w-6 h-6"></i>
</button>
</footer>
<script type="module" src="/index.js"></script> <script type="module" src="/index.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,5 +1,6 @@
import "/style.css"; // Ensure CSS is imported import "/style.css";
import Pokedex from "pokedex-promise-v2"; import Pokedex from "pokedex-promise-v2";
import annyang from "annyang";
const options = { const options = {
protocol: "https", protocol: "https",
@@ -16,6 +17,8 @@ const pokemonNameInput = document.getElementById("pokemonName");
const pokemonInfoDiv = document.getElementById("pokemonInfo"); const pokemonInfoDiv = document.getElementById("pokemonInfo");
const voiceSearchButton = document.getElementById("voiceSearchButton"); const voiceSearchButton = document.getElementById("voiceSearchButton");
const historyList = document.getElementById("historyList"); const historyList = document.getElementById("historyList");
const themeToggleButton = document.getElementById("themeToggleButton");
const themeIcon = document.getElementById("themeIcon");
// --- State --- // --- State ---
let searchHistory = []; // Now stores full data objects let searchHistory = []; // Now stores full data objects
@@ -36,8 +39,8 @@ const updateHistoryDisplay = (justAddedName = null) => {
// Determine scale based on index // Determine scale based on index
let scale = 1.0; // Default scale for the newest item let scale = 1.0; // Default scale for the newest item
if (index === 1) scale = 0.8; if (index === 1) scale = 0.9;
else if (index === 2) scale = 0.6; else if (index === 2) scale = 0.8;
// Apply scale via inline style // Apply scale via inline style
li.style.transform = `scale(${scale})`; li.style.transform = `scale(${scale})`;
@@ -137,6 +140,34 @@ const searchPokemon = () => {
pokemon.getPokemonByName(name).then(displayPokemonInfo).catch(displayError); pokemon.getPokemonByName(name).then(displayPokemonInfo).catch(displayError);
}; };
// Theme toggle logic
// Removed references to themeText as the text is no longer part of the DOM
const setTheme = (theme) => {
if (theme === "dark") {
document.documentElement.classList.add("dark");
themeIcon.classList.remove("fa-moon");
themeIcon.classList.add("fa-sun");
} else {
document.documentElement.classList.remove("dark");
themeIcon.classList.remove("fa-sun");
themeIcon.classList.add("fa-moon");
}
localStorage.setItem("theme", theme);
};
// Initialize theme based on device or saved preference
const savedTheme = localStorage.getItem("theme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
setTheme(savedTheme || (prefersDark ? "dark" : "light"));
// Toggle theme on button click
themeToggleButton.addEventListener("click", () => {
const currentTheme = document.documentElement.classList.contains("dark")
? "dark"
: "light";
setTheme(currentTheme === "dark" ? "light" : "dark");
});
// --- Event Listeners & Voice Search --- // --- Event Listeners & Voice Search ---
searchButton.addEventListener("click", searchPokemon); searchButton.addEventListener("click", searchPokemon);
@@ -158,6 +189,14 @@ if (SpeechRecognition) {
recognition.maxAlternatives = 1; recognition.maxAlternatives = 1;
voiceSearchButton.addEventListener("click", () => { voiceSearchButton.addEventListener("click", () => {
const voiceOverlay = document.getElementById("voiceOverlay");
voiceOverlay.classList.remove("hidden");
// Simulate listening for 3 seconds, then hide the overlay
setTimeout(() => {
voiceOverlay.classList.add("hidden");
}, 3000);
try { try {
pokemonNameInput.placeholder = "Listening..."; pokemonNameInput.placeholder = "Listening...";
recognition.start(); recognition.start();
@@ -192,8 +231,48 @@ if (SpeechRecognition) {
pokemonNameInput.placeholder = "Enter Pokémon name or use mic"; pokemonNameInput.placeholder = "Enter Pokémon name or use mic";
voiceSearchButton.classList.remove("text-red-500"); voiceSearchButton.classList.remove("text-red-500");
}; };
} else if (annyang) {
console.warn("Using annyang as a fallback for voice recognition.");
annyang.addCommands({
"*pokemonName": (pokemonName) => {
pokemonNameInput.value = pokemonName.toLowerCase();
searchPokemon();
},
});
voiceSearchButton.addEventListener("click", () => {
const voiceOverlay = document.getElementById("voiceOverlay");
voiceOverlay.classList.remove("hidden");
// Simulate listening for 3 seconds, then hide the overlay
setTimeout(() => {
voiceOverlay.classList.add("hidden");
}, 3000);
try {
pokemonNameInput.placeholder = "Listening...";
annyang.start();
voiceSearchButton.classList.add("text-red-500");
} catch (e) {
console.error("Annyang already started.", e);
pokemonNameInput.placeholder = "Enter Pokémon name or use mic";
}
});
annyang.addCallback("end", () => {
pokemonNameInput.placeholder = "Enter Pokémon name or use mic";
voiceSearchButton.classList.remove("text-red-500");
});
annyang.addCallback("error", (error) => {
pokemonInfoDiv.innerHTML = `<p class="text-red-500">Voice recognition error: ${error}</p>`;
console.error("Annyang error:", error);
pokemonNameInput.placeholder = "Enter Pokémon name or use mic";
voiceSearchButton.classList.remove("text-red-500");
});
} else { } else {
console.warn("Web Speech API not supported in this browser."); console.warn("Web Speech API and annyang not supported in this browser.");
voiceSearchButton.style.display = "none"; voiceSearchButton.style.display = "none";
} }

991
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,10 @@
"author": "Peter Wood <peter@peterwood.dev>", "author": "Peter Wood <peter@peterwood.dev>",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"pokedex-promise-v2": "^4.2.1" "@fortawesome/fontawesome-free": "^6.7.2",
"annyang": "^2.6.1",
"pokedex-promise-v2": "^4.2.1",
"wrangler": "^4.12.1"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.52.0", "@playwright/test": "^1.52.0",
@@ -28,4 +31,4 @@
"vite": "^6.3.2", "vite": "^6.3.2",
"vite-plugin-node-polyfills": "^0.23.0" "vite-plugin-node-polyfills": "^0.23.0"
} }
} }

View File

@@ -3,4 +3,4 @@ export default {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };

View File

@@ -1,3 +1,4 @@
@import "/node_modules/@fortawesome/fontawesome-free/css/all.min.css";
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@@ -39,3 +40,40 @@
body { body {
min-height: 100vh; min-height: 100vh;
} }
.relative.flex-grow {
position: relative;
}
#voiceSearchButton {
position: absolute;
top: 50%;
right: 0.75rem; /* Matches pr-3 */
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center; /* Ensure proper centering */
height: 100%; /* Match the input field height */
color: #6b7280; /* Matches text-gray-500 */
transition: color 0.2s;
}
#voiceSearchButton:hover {
color: #2563eb; /* Matches hover:text-blue-600 */
}
footer {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
text-align: center;
padding: 1rem;
background-color: rgba(255, 255, 255, 0.9);
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}

View File

@@ -1,9 +1,6 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: [ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
"./index.html", // Scan index.html
"./index.js", // Scan index.js for potential dynamic classes (optional but good practice)
],
theme: { theme: {
extend: {}, extend: {},
}, },