mirror of
https://github.com/acedanger/pokemon.git
synced 2025-12-05 22:50:13 -08:00
chore: initialize project with Vite, Tailwind CSS, and Pokedex API
- Added package.json with project metadata and dependencies - Created postcss.config.js for Tailwind CSS and Autoprefixer integration - Added style.css to include Tailwind's base, components, and utilities - Configured tailwind.config.js to specify content sources for class scanning - Set up vite.config.js for build configuration targeting ES2020
This commit is contained in:
41
.dockerignore
Normal file
41
.dockerignore
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# ---- Build Stage ----
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files and install dependencies
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy the rest of the application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ---- Serve Stage ----
|
||||||
|
FROM nginx:stable-alpine
|
||||||
|
|
||||||
|
# Copy built assets from the build stage
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy custom Nginx config if needed (optional, default often works for SPA)
|
||||||
|
# COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Expose port 80
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Start Nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|
||||||
|
# Ignore dependencies, build output, environment files, logs, and git directory
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
|
*.local
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.DS_Store
|
||||||
|
.git
|
||||||
|
.vscode
|
||||||
50
.gitignore
vendored
Normal file
50
.gitignore
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# ---- Build Stage ----
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files and install dependencies
|
||||||
|
# Copy package.json AND package-lock.json (if available)
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy the rest of the application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ---- Serve Stage ----
|
||||||
|
FROM nginx:stable-alpine
|
||||||
|
|
||||||
|
# Copy built assets from the build stage
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Optional: Copy a custom Nginx configuration if needed
|
||||||
|
# COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Expose port 80
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Start Nginx in the foreground
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
47
deploy.sh
Executable file
47
deploy.sh
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# This script is used to deploy the Pokemon Finder application using Docker.
|
||||||
|
# It stops any existing container, builds a new Docker image, and runs the container.
|
||||||
|
# Ensure the script is run from the directory containing the Dockerfile
|
||||||
|
# and the application code.
|
||||||
|
# Usage: sudo ./deploy.sh
|
||||||
|
|
||||||
|
# Define container and image names
|
||||||
|
CONTAINER_NAME="pokemon-app"
|
||||||
|
IMAGE_NAME="pokemon-finder"
|
||||||
|
HOST_PORT=8080
|
||||||
|
CONTAINER_PORT=80
|
||||||
|
|
||||||
|
# Stop the existing container (ignore errors if it doesn't exist)
|
||||||
|
echo "Stopping existing container: $CONTAINER_NAME..."
|
||||||
|
docker stop $CONTAINER_NAME || true
|
||||||
|
|
||||||
|
# Remove the existing container (ignore errors if it doesn't exist)
|
||||||
|
echo "Removing existing container: $CONTAINER_NAME..."
|
||||||
|
docker rm $CONTAINER_NAME || true
|
||||||
|
|
||||||
|
# Build the Docker image
|
||||||
|
echo "Building Docker image: $IMAGE_NAME..."
|
||||||
|
docker build -t $IMAGE_NAME .
|
||||||
|
|
||||||
|
# Check if build was successful
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Docker build failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the new container
|
||||||
|
echo "Running new container: $CONTAINER_NAME..."
|
||||||
|
docker run -d -p $HOST_PORT:$CONTAINER_PORT --name $CONTAINER_NAME $IMAGE_NAME
|
||||||
|
|
||||||
|
# Check if run was successful
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Docker run failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deployment complete. Application should be available at http://localhost:$HOST_PORT"
|
||||||
|
|
||||||
|
exit 0
|
||||||
62
index.html
Normal file
62
index.html
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Pokémon Finder</title>
|
||||||
|
<link rel="stylesheet" href="/style.css" />
|
||||||
|
</head>
|
||||||
|
<body
|
||||||
|
class="bg-gradient-to-r from-blue-400 via-purple-500 to-pink-500 min-h-screen flex items-center justify-center p-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-white bg-opacity-90 rounded-lg shadow-xl p-6 md:p-10 w-full max-w-md"
|
||||||
|
>
|
||||||
|
<h1 class="text-3xl md:text-4xl font-bold text-center text-gray-800 mb-6">
|
||||||
|
Find Your Pokémon!
|
||||||
|
</h1>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 mb-6">
|
||||||
|
<!-- Wrap input and voice button -->
|
||||||
|
<div class="relative flex-grow">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="pokemonName"
|
||||||
|
placeholder="Enter Pokémon name or use mic"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<!-- Microphone Button -->
|
||||||
|
<button
|
||||||
|
id="voiceSearchButton"
|
||||||
|
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-500 hover:text-blue-600 focus:outline-none"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="searchButton"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md transition duration-200 shadow-md"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="pokemonInfo" class="mt-6 text-center text-gray-700">
|
||||||
|
<!-- Pokémon info will be displayed here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="/index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
108
index.js
Normal file
108
index.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import "/style.css"; // Import the CSS file
|
||||||
|
import Pokedex from "pokedex-promise-v2"; // Use bare specifier
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
protocol: "https",
|
||||||
|
hostName: "pokeapi.co", // Use the actual PokeAPI host
|
||||||
|
versionPath: "/api/v2/",
|
||||||
|
cacheLimit: 100 * 1000, // 100s
|
||||||
|
timeout: 10 * 1000, // 10s increased timeout
|
||||||
|
};
|
||||||
|
const pokemon = new Pokedex(options);
|
||||||
|
|
||||||
|
const searchButton = document.getElementById("searchButton");
|
||||||
|
const pokemonNameInput = document.getElementById("pokemonName");
|
||||||
|
const pokemonInfoDiv = document.getElementById("pokemonInfo");
|
||||||
|
const voiceSearchButton = document.getElementById("voiceSearchButton"); // Get voice button
|
||||||
|
|
||||||
|
const displayPokemonInfo = (data) => {
|
||||||
|
pokemonInfoDiv.innerHTML = `
|
||||||
|
<h2 class="text-2xl font-semibold mb-2">${
|
||||||
|
data.name.charAt(0).toUpperCase() + data.name.slice(1)
|
||||||
|
}</h2>
|
||||||
|
<img src="${data.sprites.front_default}" alt="${
|
||||||
|
data.name
|
||||||
|
}" class="mx-auto mb-4 w-32 h-32">
|
||||||
|
<p><strong>Type(s):</strong> ${data.types
|
||||||
|
.map((typeInfo) => typeInfo.type.name)
|
||||||
|
.join(", ")}</p>
|
||||||
|
<p><strong>Height:</strong> ${data.height / 10} m</p>
|
||||||
|
<p><strong>Weight:</strong> ${data.weight / 10} kg</p>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayError = (error) => {
|
||||||
|
pokemonInfoDiv.innerHTML = `<p class="text-red-500">Error: ${error.message}</p>`;
|
||||||
|
console.error(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchPokemon = () => {
|
||||||
|
const name = pokemonNameInput.value.trim().toLowerCase();
|
||||||
|
if (!name) {
|
||||||
|
pokemonInfoDiv.innerHTML = `<p class="text-yellow-600">Please enter a Pokémon name.</p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pokemonInfoDiv.innerHTML = `<p>Loading...</p>`; // Provide loading feedback
|
||||||
|
pokemon.getPokemonByName(name).then(displayPokemonInfo).catch(displayError);
|
||||||
|
};
|
||||||
|
|
||||||
|
searchButton.addEventListener("click", searchPokemon);
|
||||||
|
|
||||||
|
pokemonNameInput.addEventListener("keypress", (event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
searchPokemon();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Voice Search ---
|
||||||
|
const SpeechRecognition =
|
||||||
|
window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
|
let recognition;
|
||||||
|
|
||||||
|
if (SpeechRecognition) {
|
||||||
|
recognition = new SpeechRecognition();
|
||||||
|
recognition.continuous = false; // Only listen for a single utterance
|
||||||
|
recognition.lang = "en-US";
|
||||||
|
recognition.interimResults = false; // We only want final results
|
||||||
|
recognition.maxAlternatives = 1;
|
||||||
|
|
||||||
|
voiceSearchButton.addEventListener("click", () => {
|
||||||
|
try {
|
||||||
|
pokemonNameInput.placeholder = "Listening...";
|
||||||
|
recognition.start();
|
||||||
|
voiceSearchButton.classList.add("text-red-500"); // Indicate listening
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Speech recognition already started.", e);
|
||||||
|
pokemonNameInput.placeholder = "Enter Pokémon name or use mic"; // Reset placeholder if error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
recognition.onresult = (event) => {
|
||||||
|
const speechResult = event.results[0][0].transcript.toLowerCase();
|
||||||
|
pokemonNameInput.value = speechResult;
|
||||||
|
// Optionally trigger search immediately after recognition
|
||||||
|
searchPokemon();
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onspeechend = () => {
|
||||||
|
recognition.stop();
|
||||||
|
pokemonNameInput.placeholder = "Enter Pokémon name or use mic";
|
||||||
|
voiceSearchButton.classList.remove("text-red-500"); // Remove listening indicator
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onerror = (event) => {
|
||||||
|
pokemonInfoDiv.innerHTML = `<p class="text-red-500">Voice recognition error: ${event.error}</p>`;
|
||||||
|
console.error("Speech recognition error:", event);
|
||||||
|
pokemonNameInput.placeholder = "Enter Pokémon name or use mic";
|
||||||
|
voiceSearchButton.classList.remove("text-red-500"); // Remove listening indicator
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onnomatch = (event) => {
|
||||||
|
pokemonInfoDiv.innerHTML = `<p class="text-yellow-600">Didn't recognize that Pokémon name.</p>`;
|
||||||
|
pokemonNameInput.placeholder = "Enter Pokémon name or use mic";
|
||||||
|
voiceSearchButton.classList.remove("text-red-500"); // Remove listening indicator
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.warn("Web Speech API not supported in this browser.");
|
||||||
|
voiceSearchButton.style.display = "none"; // Hide button if not supported
|
||||||
|
}
|
||||||
2981
package-lock.json
generated
Normal file
2981
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
Normal file
30
package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "pokemon",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"description": "Pokemon for Margot",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"pokemon",
|
||||||
|
"margot"
|
||||||
|
],
|
||||||
|
"author": "Peter Wood <peter@peterwood.dev>",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"pokedex-promise-v2": "^4.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.52.0",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"postcss": "^8.5.3",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"vite": "^6.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
9
style.css
Normal file
9
style.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Add any custom base styles here if needed */
|
||||||
|
body {
|
||||||
|
/* Example: Ensure gradient covers full height */
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
11
tailwind.config.js
Normal file
11
tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html", // Scan index.html
|
||||||
|
"./index.js", // Scan index.js for potential dynamic classes (optional but good practice)
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
7
vite.config.js
Normal file
7
vite.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
target: "es2020", // Or try 'es2020' or 'modules' if 'esnext' doesn't work
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user