mirror of
https://github.com/CarGDev/debug-dojo.git
synced 2025-09-18 18:08:37 +00:00
feat: add autocomplete search with Zustand store and API layer
- Add PokemonAutoComplete atom component using Ant Design - Implement API layer with axios for Pokémon data fetching - Create Zustand store for centralized state management - Add autocomplete suggestions with real-time filtering - Update SearchBar molecule to use autocomplete functionality - Load all Pokémon names on app initialization - Add path aliases for cleaner imports (@api, @stores) - Remove old service layer in favor of API layer - Maintain intentional bugs for debugging practice - Update project structure and documentation
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
112
README.md
Normal file
112
README.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# Pokémon Explorer App
|
||||||
|
|
||||||
|
A Vite + React + TypeScript application that allows users to search for Pokémon and view their details using the PokeAPI.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Search for Pokémon by name with autocomplete suggestions
|
||||||
|
- Display Pokémon details including:
|
||||||
|
- Name and image
|
||||||
|
- Types
|
||||||
|
- Base stats with progress bars
|
||||||
|
- Height and weight
|
||||||
|
- Modern UI built with Ant Design
|
||||||
|
- Atomic Design architecture
|
||||||
|
- Centralized state management with Zustand
|
||||||
|
- API layer with axios for data fetching
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── api/ # API layer with axios
|
||||||
|
│ ├── config.ts # Axios configuration
|
||||||
|
│ ├── get/ # GET requests
|
||||||
|
│ ├── post/ # POST requests
|
||||||
|
│ ├── put/ # PUT requests
|
||||||
|
│ ├── delete/ # DELETE requests
|
||||||
|
│ └── index.ts # API exports
|
||||||
|
├── components/
|
||||||
|
│ ├── atoms/ # Basic UI elements (Input, Button, AutoComplete)
|
||||||
|
│ ├── molecules/ # Combinations (SearchBar)
|
||||||
|
│ ├── organisms/ # Complex components (PokemonDetails)
|
||||||
|
│ ├── templates/ # Layout structure (MainLayout)
|
||||||
|
│ └── pages/ # Route-level views (Home)
|
||||||
|
├── stores/ # Zustand state management
|
||||||
|
│ ├── pokemonStore.ts # Pokémon state store
|
||||||
|
│ └── index.ts # Store exports
|
||||||
|
├── styles/ # SCSS modules
|
||||||
|
│ ├── global.module.scss # Global styles
|
||||||
|
│ ├── app.module.scss # App-specific styles
|
||||||
|
│ └── index.ts # Style exports
|
||||||
|
└── types/ # TypeScript type definitions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Path Aliases
|
||||||
|
|
||||||
|
The project uses path aliases for cleaner imports:
|
||||||
|
|
||||||
|
- `@/` - Points to `src/`
|
||||||
|
- `@components/` - Points to `src/components/`
|
||||||
|
- `@styles/` - Points to `src/styles/`
|
||||||
|
- `@pokemonTypes/` - Points to `src/types/`
|
||||||
|
- `@api/` - Points to `src/api/`
|
||||||
|
- `@stores/` - Points to `src/stores/`
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
```typescript
|
||||||
|
// Instead of: import { PokemonInput } from '../../components/atoms/PokemonInput';
|
||||||
|
import { PokemonInput } from '@components/atoms/PokemonInput';
|
||||||
|
|
||||||
|
// Instead of: import styles from '../../styles/app.module.scss';
|
||||||
|
import styles from '@styles/app.module.scss';
|
||||||
|
|
||||||
|
// Instead of: import { getPokemon } from '../../api/get/pokemon';
|
||||||
|
import { getPokemon } from '@api/get/pokemon';
|
||||||
|
|
||||||
|
// Instead of: import { usePokemonStore } from '../../stores/pokemonStore';
|
||||||
|
import { usePokemonStore } from '@stores/pokemonStore';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Intentional Bugs (For Debugging Session)
|
||||||
|
|
||||||
|
⚠️ **These bugs are intentionally left in the code for debugging practice:**
|
||||||
|
|
||||||
|
1. **Silent Failure**: When a Pokémon doesn't exist, no error message is shown - the app silently fails
|
||||||
|
2. **UI Flickering**: When searching for a new Pokémon, the previous data briefly disappears causing a flicker effect
|
||||||
|
3. **Incorrect Stat Mapping**: Speed and defense stats are swapped in the display
|
||||||
|
4. **TypeScript Error**: The `height` field is assumed to always be present when it's actually optional in the API
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start the development server:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Open your browser and navigate to the URL shown in the terminal
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Enter a Pokémon name in the search bar (e.g., "pikachu", "charizard", "bulbasaur")
|
||||||
|
2. Click "Search" or press Enter
|
||||||
|
3. View the Pokémon's details including stats, types, and image
|
||||||
|
|
||||||
|
## Technologies Used
|
||||||
|
|
||||||
|
- **Vite** - Build tool and dev server
|
||||||
|
- **React** - UI framework
|
||||||
|
- **TypeScript** - Type safety
|
||||||
|
- **Ant Design** - UI component library
|
||||||
|
- **PokeAPI** - Pokémon data source
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
- `npm run dev` - Start development server
|
||||||
|
- `npm run build` - Build for production
|
||||||
|
- `npm run preview` - Preview production build
|
23
eslint.config.js
Normal file
23
eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default tseslint.config([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs['recommended-latest'],
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
5078
package-lock.json
generated
Normal file
5078
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "pokemon-app",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "^24.1.0",
|
||||||
|
"antd": "^5.26.7",
|
||||||
|
"axios": "^1.11.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"sass": "^1.89.2",
|
||||||
|
"zustand": "^5.0.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.30.1",
|
||||||
|
"@types/react": "^19.1.8",
|
||||||
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
|
"eslint": "^9.30.1",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
|
"globals": "^16.3.0",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"typescript-eslint": "^8.35.1",
|
||||||
|
"vite": "^7.0.4"
|
||||||
|
}
|
||||||
|
}
|
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
24
src/App.tsx
Normal file
24
src/App.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { ConfigProvider } from 'antd';
|
||||||
|
import { MainLayout } from '@components/templates/MainLayout';
|
||||||
|
import { Home } from '@components/pages/Home';
|
||||||
|
import styles from '@styles/app.module.scss';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: {
|
||||||
|
colorPrimary: '#1890ff',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.app}>
|
||||||
|
<MainLayout>
|
||||||
|
<Home />
|
||||||
|
</MainLayout>
|
||||||
|
</div>
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
37
src/api/config.ts
Normal file
37
src/api/config.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const BASE_URL = 'https://pokeapi.co/api/v2';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: BASE_URL,
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request interceptor
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
console.log(`Making ${config.method?.toUpperCase()} request to: ${config.url}`);
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Request error:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Response interceptor
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
console.log(`Response received from: ${response.config.url}`, response.status);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Response error:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default api;
|
34
src/api/get/pokemon.ts
Normal file
34
src/api/get/pokemon.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import api from '../config';
|
||||||
|
import type { Pokemon } from '@pokemonTypes/pokemon';
|
||||||
|
|
||||||
|
export const getPokemon = async (name: string): Promise<Pokemon> => {
|
||||||
|
try {
|
||||||
|
const response = await api.get<Pokemon>(`/pokemon/${name.toLowerCase()}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
// Intentional bug #1: No proper error handling for non-existent Pokémon
|
||||||
|
console.error('Error fetching Pokémon:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPokemonList = async (limit: number = 20, offset: number = 0) => {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/pokemon?limit=${limit}&offset=${offset}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching Pokémon list:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllPokemonNames = async (): Promise<string[]> => {
|
||||||
|
try {
|
||||||
|
// Fetch all Pokémon names (151 total in the original Pokédex)
|
||||||
|
const response = await api.get('/pokemon?limit=151&offset=0');
|
||||||
|
return response.data.results.map((pokemon: { name: string }) => pokemon.name);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching all Pokémon names:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
14
src/api/index.ts
Normal file
14
src/api/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// API configuration
|
||||||
|
export { default as api } from './config';
|
||||||
|
|
||||||
|
// GET requests
|
||||||
|
export * from './get/pokemon';
|
||||||
|
|
||||||
|
// POST requests (for future use)
|
||||||
|
// export * from './post/';
|
||||||
|
|
||||||
|
// PUT requests (for future use)
|
||||||
|
// export * from './put/';
|
||||||
|
|
||||||
|
// DELETE requests (for future use)
|
||||||
|
// export * from './delete/';
|
54
src/components/atoms/PokemonAutoComplete.tsx
Normal file
54
src/components/atoms/PokemonAutoComplete.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { AutoComplete, Input } from 'antd';
|
||||||
|
import type { AutoCompleteProps } from 'antd';
|
||||||
|
|
||||||
|
interface PokemonAutoCompleteProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onSearch: (value: string) => void;
|
||||||
|
options: { value: string; label: string }[];
|
||||||
|
loading?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
onSelect?: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PokemonAutoComplete: React.FC<PokemonAutoCompleteProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onSearch,
|
||||||
|
options,
|
||||||
|
loading = false,
|
||||||
|
placeholder = 'Enter Pokémon name...',
|
||||||
|
onSelect,
|
||||||
|
}) => {
|
||||||
|
const handleSearch = (searchText: string) => {
|
||||||
|
onChange(searchText);
|
||||||
|
onSearch(searchText);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (selectedValue: string) => {
|
||||||
|
onChange(selectedValue);
|
||||||
|
onSelect?.(selectedValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutoComplete
|
||||||
|
value={value}
|
||||||
|
options={options}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
onChange={onChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
notFoundContent={loading ? 'Loading...' : 'No Pokémon found'}
|
||||||
|
filterOption={false} // Disable built-in filtering since we handle it in the store
|
||||||
|
>
|
||||||
|
<Input.Search
|
||||||
|
size="large"
|
||||||
|
allowClear
|
||||||
|
loading={loading}
|
||||||
|
enterButton
|
||||||
|
/>
|
||||||
|
</AutoComplete>
|
||||||
|
);
|
||||||
|
};
|
30
src/components/atoms/PokemonButton.tsx
Normal file
30
src/components/atoms/PokemonButton.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
|
||||||
|
interface PokemonButtonProps {
|
||||||
|
onClick: () => void;
|
||||||
|
loading?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
type?: 'primary' | 'default' | 'dashed' | 'link' | 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PokemonButton: React.FC<PokemonButtonProps> = ({
|
||||||
|
onClick,
|
||||||
|
loading = false,
|
||||||
|
disabled = false,
|
||||||
|
children,
|
||||||
|
type = 'primary'
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type={type}
|
||||||
|
onClick={onClick}
|
||||||
|
loading={loading}
|
||||||
|
disabled={disabled}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
24
src/components/atoms/PokemonInput.tsx
Normal file
24
src/components/atoms/PokemonInput.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Input } from 'antd';
|
||||||
|
|
||||||
|
interface PokemonInputProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PokemonInput: React.FC<PokemonInputProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = 'Enter Pokémon name...'
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
size="large"
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
16
src/components/index.ts
Normal file
16
src/components/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// Atoms
|
||||||
|
export { PokemonInput } from './atoms/PokemonInput';
|
||||||
|
export { PokemonButton } from './atoms/PokemonButton';
|
||||||
|
export { PokemonAutoComplete } from './atoms/PokemonAutoComplete';
|
||||||
|
|
||||||
|
// Molecules
|
||||||
|
export { SearchBar } from './molecules/SearchBar';
|
||||||
|
|
||||||
|
// Organisms
|
||||||
|
export { PokemonDetails } from './organisms/PokemonDetails';
|
||||||
|
|
||||||
|
// Templates
|
||||||
|
export { MainLayout } from './templates/MainLayout';
|
||||||
|
|
||||||
|
// Pages
|
||||||
|
export { Home } from './pages/Home';
|
32
src/components/molecules/SearchBar.tsx
Normal file
32
src/components/molecules/SearchBar.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PokemonAutoComplete } from '@components/atoms/PokemonAutoComplete';
|
||||||
|
|
||||||
|
interface SearchBarProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onSearch: (value: string) => void;
|
||||||
|
onSelect?: (value: string) => void;
|
||||||
|
options: { value: string; label: string }[];
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchBar: React.FC<SearchBarProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onSearch,
|
||||||
|
onSelect,
|
||||||
|
options,
|
||||||
|
loading = false
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<PokemonAutoComplete
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onSearch={onSearch}
|
||||||
|
onSelect={onSelect}
|
||||||
|
options={options}
|
||||||
|
loading={loading}
|
||||||
|
placeholder="Enter Pokémon name..."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
95
src/components/organisms/PokemonDetails.tsx
Normal file
95
src/components/organisms/PokemonDetails.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, Row, Col, Tag, Progress, Typography, Image } from 'antd';
|
||||||
|
import type { Pokemon } from '@pokemonTypes/pokemon';
|
||||||
|
import styles from '@styles/app.module.scss';
|
||||||
|
|
||||||
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
interface PokemonDetailsProps {
|
||||||
|
pokemon: Pokemon | null;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PokemonDetails: React.FC<PokemonDetailsProps> = ({
|
||||||
|
pokemon,
|
||||||
|
loading = false
|
||||||
|
}) => {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Card loading={true} className={styles.pokemonCard}>
|
||||||
|
<div style={{ height: 400 }} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pokemon) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intentional bug #3: Incorrect stat mapping
|
||||||
|
// Speed and defense are swapped in the display
|
||||||
|
const getStatName = (statName: string): string => {
|
||||||
|
switch (statName) {
|
||||||
|
case 'speed':
|
||||||
|
return 'defense'; // Bug: showing defense instead of speed
|
||||||
|
case 'defense':
|
||||||
|
return 'speed'; // Bug: showing speed instead of defense
|
||||||
|
default:
|
||||||
|
return statName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={styles.pokemonCard}>
|
||||||
|
<Row gutter={[24, 24]}>
|
||||||
|
<Col xs={24} md={8}>
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<Image
|
||||||
|
src={pokemon.sprites.front_default}
|
||||||
|
alt={pokemon.name}
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
className={styles.pokemonImage}
|
||||||
|
/>
|
||||||
|
<Title level={2} className={styles.pokemonName}>
|
||||||
|
{pokemon.name}
|
||||||
|
</Title>
|
||||||
|
<div className={styles.pokemonTypes}>
|
||||||
|
{pokemon.types.map((type) => (
|
||||||
|
<Tag key={type.type.name} color="blue" className={styles.typeTag}>
|
||||||
|
{type.type.name.toUpperCase()}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Text type="secondary">
|
||||||
|
Height: {pokemon.height / 10}m | Weight: {pokemon.weight / 10}kg
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} md={16}>
|
||||||
|
<Title level={3}>Base Stats</Title>
|
||||||
|
<div className={styles.pokemonStats}>
|
||||||
|
{pokemon.stats.map((stat) => (
|
||||||
|
<div key={stat.stat.name} className={styles.statItem}>
|
||||||
|
<div className={styles.statHeader}>
|
||||||
|
<Text strong className={styles.statName}>
|
||||||
|
{getStatName(stat.stat.name)}:
|
||||||
|
</Text>
|
||||||
|
<Text>{stat.base_stat}</Text>
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
percent={(stat.base_stat / 255) * 100}
|
||||||
|
showInfo={false}
|
||||||
|
strokeColor={{
|
||||||
|
'0%': '#108ee9',
|
||||||
|
'100%': '#87d068',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
88
src/components/pages/Home.tsx
Normal file
88
src/components/pages/Home.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { SearchBar } from '@components/molecules/SearchBar';
|
||||||
|
import { PokemonDetails } from '@components/organisms/PokemonDetails';
|
||||||
|
import { usePokemonStore } from '@stores/pokemonStore';
|
||||||
|
import styles from '@styles/app.module.scss';
|
||||||
|
|
||||||
|
export const Home: React.FC = () => {
|
||||||
|
const {
|
||||||
|
pokemon,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
searchTerm,
|
||||||
|
suggestions,
|
||||||
|
suggestionsLoading,
|
||||||
|
setSearchTerm,
|
||||||
|
fetchPokemon,
|
||||||
|
fetchSuggestions,
|
||||||
|
loadAllPokemonNames,
|
||||||
|
clearError,
|
||||||
|
} = usePokemonStore();
|
||||||
|
|
||||||
|
// Load all Pokémon names on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
loadAllPokemonNames();
|
||||||
|
}, [loadAllPokemonNames]);
|
||||||
|
|
||||||
|
const handleSearch = async (value: string) => {
|
||||||
|
if (!value.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetchPokemon(value);
|
||||||
|
if (pokemon) {
|
||||||
|
message.success(`Found ${pokemon.name}!`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in handleSearch:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchTermChange = (value: string) => {
|
||||||
|
setSearchTerm(value);
|
||||||
|
if (error) {
|
||||||
|
clearError();
|
||||||
|
}
|
||||||
|
// Fetch suggestions as user types
|
||||||
|
fetchSuggestions(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (selectedValue: string) => {
|
||||||
|
setSearchTerm(selectedValue);
|
||||||
|
handleSearch(selectedValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert suggestions to AutoComplete options format
|
||||||
|
const options = suggestions.map(name => ({
|
||||||
|
value: name,
|
||||||
|
label: name.charAt(0).toUpperCase() + name.slice(1), // Capitalize first letter
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.searchSection}>
|
||||||
|
<h1>
|
||||||
|
Search for your favorite Pokémon!
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<SearchBar
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={handleSearchTermChange}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
options={options}
|
||||||
|
loading={suggestionsLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div style={{ color: 'red', textAlign: 'center', marginTop: 16 }}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PokemonDetails
|
||||||
|
pokemon={pokemon}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
27
src/components/templates/MainLayout.tsx
Normal file
27
src/components/templates/MainLayout.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Layout, Typography } from 'antd';
|
||||||
|
import styles from '@styles/app.module.scss';
|
||||||
|
|
||||||
|
const { Header, Content } = Layout;
|
||||||
|
const { Title } = Typography;
|
||||||
|
|
||||||
|
interface MainLayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<Layout style={{ minHeight: '100vh' }}>
|
||||||
|
<Header className={styles.header}>
|
||||||
|
<Title level={3} className={styles.title}>
|
||||||
|
🎮 Pokémon Explorer
|
||||||
|
</Title>
|
||||||
|
</Header>
|
||||||
|
<Content className={styles.content}>
|
||||||
|
<div className={styles.container}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import '@styles/global.module.scss'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
2
src/stores/index.ts
Normal file
2
src/stores/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Stores
|
||||||
|
export { usePokemonStore } from './pokemonStore';
|
117
src/stores/pokemonStore.ts
Normal file
117
src/stores/pokemonStore.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { getPokemon, getAllPokemonNames } from '@api/get/pokemon';
|
||||||
|
import type { Pokemon } from '@pokemonTypes/pokemon';
|
||||||
|
|
||||||
|
interface PokemonState {
|
||||||
|
// State
|
||||||
|
pokemon: Pokemon | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
searchTerm: string;
|
||||||
|
suggestions: string[];
|
||||||
|
suggestionsLoading: boolean;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setPokemon: (pokemon: Pokemon | null) => void;
|
||||||
|
setLoading: (loading: boolean) => void;
|
||||||
|
setError: (error: string | null) => void;
|
||||||
|
setSearchTerm: (term: string) => void;
|
||||||
|
setSuggestions: (suggestions: string[]) => void;
|
||||||
|
setSuggestionsLoading: (loading: boolean) => void;
|
||||||
|
|
||||||
|
// Async actions
|
||||||
|
fetchPokemon: (name: string) => Promise<void>;
|
||||||
|
fetchSuggestions: (searchTerm: string) => Promise<void>;
|
||||||
|
loadAllPokemonNames: () => Promise<void>;
|
||||||
|
clearPokemon: () => void;
|
||||||
|
clearError: () => void;
|
||||||
|
clearSuggestions: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePokemonStore = create<PokemonState>((set, get) => ({
|
||||||
|
// Initial state
|
||||||
|
pokemon: null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
searchTerm: '',
|
||||||
|
suggestions: [],
|
||||||
|
suggestionsLoading: false,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setPokemon: (pokemon) => set({ pokemon }),
|
||||||
|
setLoading: (loading) => set({ loading }),
|
||||||
|
setError: (error) => set({ error }),
|
||||||
|
setSearchTerm: (searchTerm) => set({ searchTerm }),
|
||||||
|
setSuggestions: (suggestions) => set({ suggestions }),
|
||||||
|
setSuggestionsLoading: (suggestionsLoading) => set({ suggestionsLoading }),
|
||||||
|
|
||||||
|
// Async actions
|
||||||
|
fetchPokemon: async (name: string) => {
|
||||||
|
const { setLoading, setPokemon, setError } = get();
|
||||||
|
|
||||||
|
if (!name.trim()) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Intentional bug #2: UI flickering
|
||||||
|
// Setting pokemon to null first causes the previous data to disappear briefly
|
||||||
|
setPokemon(null);
|
||||||
|
|
||||||
|
const pokemon = await getPokemon(name);
|
||||||
|
setPokemon(pokemon);
|
||||||
|
} catch (error) {
|
||||||
|
// Intentional bug #1: No error handling for non-existent Pokémon
|
||||||
|
// This will set a generic error message instead of handling specific cases
|
||||||
|
setError('Failed to fetch Pokémon data');
|
||||||
|
console.error('Error in fetchPokemon:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchSuggestions: async (searchTerm: string) => {
|
||||||
|
const { setSuggestions, setSuggestionsLoading } = get();
|
||||||
|
|
||||||
|
if (!searchTerm.trim()) {
|
||||||
|
setSuggestions([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuggestionsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const allNames = await getAllPokemonNames();
|
||||||
|
const filtered = allNames
|
||||||
|
.filter(name => name.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||||
|
.slice(0, 10); // Limit to 10 suggestions
|
||||||
|
setSuggestions(filtered);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching suggestions:', error);
|
||||||
|
setSuggestions([]);
|
||||||
|
} finally {
|
||||||
|
setSuggestionsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loadAllPokemonNames: async () => {
|
||||||
|
const { setSuggestions, setSuggestionsLoading } = get();
|
||||||
|
|
||||||
|
setSuggestionsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const allNames = await getAllPokemonNames();
|
||||||
|
setSuggestions(allNames);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading all Pokémon names:', error);
|
||||||
|
setSuggestions([]);
|
||||||
|
} finally {
|
||||||
|
setSuggestionsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearPokemon: () => set({ pokemon: null }),
|
||||||
|
clearError: () => set({ error: null }),
|
||||||
|
clearSuggestions: () => set({ suggestions: [] }),
|
||||||
|
}));
|
79
src/styles/app.module.scss
Normal file
79
src/styles/app.module.scss
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
.app {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: #001529;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 24px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: white;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 24px;
|
||||||
|
background: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchSection {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemonCard {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemonImage {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemonName {
|
||||||
|
text-transform: capitalize;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemonTypes {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.typeTag {
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemonStats {
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.statItem {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.statHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
.statName {
|
||||||
|
text-transform: capitalize;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
src/styles/global.module.scss
Normal file
20
src/styles/global.module.scss
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
@import 'antd/dist/reset.css';
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
5
src/styles/index.ts
Normal file
5
src/styles/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Global styles
|
||||||
|
export { default as globalStyles } from '@styles/global.module.scss';
|
||||||
|
|
||||||
|
// App-specific styles
|
||||||
|
export { default as appStyles } from '@styles/app.module.scss';
|
39
src/types/pokemon.ts
Normal file
39
src/types/pokemon.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export interface PokemonType {
|
||||||
|
slot: number;
|
||||||
|
type: {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PokemonStat {
|
||||||
|
base_stat: number;
|
||||||
|
effort: number;
|
||||||
|
stat: {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PokemonSprites {
|
||||||
|
front_default: string;
|
||||||
|
front_shiny?: string;
|
||||||
|
back_default?: string;
|
||||||
|
back_shiny?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Pokemon {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
height: number; // Intentional bug: height is actually optional in the API
|
||||||
|
weight: number;
|
||||||
|
types: PokemonType[];
|
||||||
|
stats: PokemonStat[];
|
||||||
|
sprites: PokemonSprites;
|
||||||
|
base_experience?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PokemonApiResponse {
|
||||||
|
data: Pokemon;
|
||||||
|
status: number;
|
||||||
|
}
|
9
src/types/scss.d.ts
vendored
Normal file
9
src/types/scss.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
declare module '*.module.scss' {
|
||||||
|
const classes: { [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.scss' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
38
tsconfig.app.json
Normal file
38
tsconfig.app.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
|
||||||
|
/* Path mapping */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
"@components/*": ["src/components/*"],
|
||||||
|
"@styles/*": ["src/styles/*"],
|
||||||
|
"@pokemonTypes/*": ["src/types/*"],
|
||||||
|
"@api/*": ["src/api/*"],
|
||||||
|
"@stores/*": ["src/stores/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
25
tsconfig.node.json
Normal file
25
tsconfig.node.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
18
vite.config.ts
Normal file
18
vite.config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
'@components': path.resolve(__dirname, './src/components'),
|
||||||
|
'@styles': path.resolve(__dirname, './src/styles'),
|
||||||
|
'@pokemonTypes': path.resolve(__dirname, './src/types'),
|
||||||
|
'@api': path.resolve(__dirname, './src/api'),
|
||||||
|
'@stores': path.resolve(__dirname, './src/stores'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
Reference in New Issue
Block a user