initialize Vite + React + TypeScript project with Tailwind CSS and API service setup
This commit is contained in:
commit
f133e18302
24
.gitignore
vendored
Executable file
24
.gitignore
vendored
Executable 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?
|
28
eslint.config.js
Executable file
28
eslint.config.js
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
13
index.html
Executable file
13
index.html
Executable 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>
|
4142
package-lock.json
generated
Executable file
4142
package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load Diff
34
package.json
Executable file
34
package.json
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-react-typescript-starter",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.6.7",
|
||||||
|
"lucide-react": "^0.344.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.9.1",
|
||||||
|
"@types/react": "^18.3.5",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"autoprefixer": "^10.4.18",
|
||||||
|
"eslint": "^9.9.1",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.11",
|
||||||
|
"globals": "^15.9.0",
|
||||||
|
"postcss": "^8.4.35",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.5.3",
|
||||||
|
"typescript-eslint": "^8.3.0",
|
||||||
|
"vite": "^5.4.2"
|
||||||
|
}
|
||||||
|
}
|
6
postcss.config.js
Executable file
6
postcss.config.js
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
203
src/App.tsx
Executable file
203
src/App.tsx
Executable file
@ -0,0 +1,203 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import ClassSelection from './components/ClassSelection';
|
||||||
|
import PetDisplay from './components/PetDisplay';
|
||||||
|
import InteractionMenu from './components/InteractionMenu';
|
||||||
|
import { Pet, PetClassInfo } from './types/Pet';
|
||||||
|
import { fetchPets, createPet } from './services/api/api';
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [pet, setPet] = useState<Pet | null>(null);
|
||||||
|
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||||
|
const [showNameModal, setShowNameModal] = useState(false);
|
||||||
|
const [petName, setPetName] = useState('');
|
||||||
|
const [selectedClass, setSelectedClass] = useState<{ key: string; info: PetClassInfo } | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadPets = async () => {
|
||||||
|
try {
|
||||||
|
const pets = await fetchPets();
|
||||||
|
if (pets.length > 0) {
|
||||||
|
setPet(pets[0]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading pets:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPets();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClassSelect = (classKey: string, classInfo: PetClassInfo) => {
|
||||||
|
setSelectedClass({ key: classKey, info: classInfo });
|
||||||
|
setShowConfirmation(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (!selectedClass) return;
|
||||||
|
setShowConfirmation(false);
|
||||||
|
setShowNameModal(true);
|
||||||
|
setPetName('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNameSubmit = async () => {
|
||||||
|
if (!selectedClass || !petName.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newPet = await createPet({
|
||||||
|
name: petName.trim(),
|
||||||
|
class: selectedClass.key,
|
||||||
|
});
|
||||||
|
setPet(newPet);
|
||||||
|
setShowNameModal(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating pet:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFeed = () => {
|
||||||
|
if (!pet) return;
|
||||||
|
setPet({
|
||||||
|
...pet,
|
||||||
|
stats: {
|
||||||
|
...pet.stats,
|
||||||
|
strength: Math.min(100, pet.stats.strength + 5)
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
...pet.resources,
|
||||||
|
food: Math.max(0, pet.resources.food - 10)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlay = () => {
|
||||||
|
if (!pet) return;
|
||||||
|
setPet({
|
||||||
|
...pet,
|
||||||
|
stats: {
|
||||||
|
...pet.stats,
|
||||||
|
charisma: Math.min(100, pet.stats.charisma + 5)
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
...pet.resources,
|
||||||
|
wisdom: pet.resources.wisdom + 5
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSleep = () => {
|
||||||
|
if (!pet) return;
|
||||||
|
setPet({
|
||||||
|
...pet,
|
||||||
|
stats: {
|
||||||
|
intelligence: Math.min(100, pet.stats.intelligence + 5),
|
||||||
|
strength: Math.min(100, pet.stats.strength + 2),
|
||||||
|
charisma: Math.min(100, pet.stats.charisma + 2)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomize = () => {
|
||||||
|
console.log('Customize pet');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!pet) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 text-white">
|
||||||
|
{showNameModal && selectedClass ? (
|
||||||
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
||||||
|
<div className="bg-gray-800 p-6 rounded-xl max-w-md w-full">
|
||||||
|
<h2 className="text-2xl font-bold mb-2">Name Your Pet</h2>
|
||||||
|
<p className="text-gray-400 mb-6">
|
||||||
|
Creating a new {selectedClass.info.name.toLowerCase()} pet
|
||||||
|
</p>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="petName" className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Pet Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="petName"
|
||||||
|
value={petName}
|
||||||
|
onChange={(e) => setPetName(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="Enter a name for your pet"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<button
|
||||||
|
onClick={handleNameSubmit}
|
||||||
|
disabled={!petName.trim()}
|
||||||
|
className={`flex-1 px-4 py-2 rounded-lg ${
|
||||||
|
petName.trim()
|
||||||
|
? 'bg-blue-600 hover:bg-blue-700'
|
||||||
|
: 'bg-blue-600/50 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Create Pet
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowNameModal(false);
|
||||||
|
setSelectedClass(null);
|
||||||
|
}}
|
||||||
|
className="flex-1 px-4 py-2 bg-gray-600 hover:bg-gray-700 rounded-lg"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : showConfirmation && selectedClass ? (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4">
|
||||||
|
<div className="bg-gray-800 p-6 rounded-xl max-w-md w-full">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Confirm Selection</h2>
|
||||||
|
<p className="mb-4">Are you sure you want to choose {selectedClass.info.name}?</p>
|
||||||
|
<div className="space-y-2 mb-6">
|
||||||
|
{selectedClass.info.modifiers.map((modifier, index) => (
|
||||||
|
<div key={index} className="text-sm text-gray-300">• {modifier}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<button
|
||||||
|
onClick={handleConfirm}
|
||||||
|
className="flex-1 bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded"
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowConfirmation(false);
|
||||||
|
setSelectedClass(null);
|
||||||
|
}}
|
||||||
|
className="flex-1 bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ClassSelection onSelect={handleClassSelect} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 text-white p-4">
|
||||||
|
<div className="max-w-4xl mx-auto grid gap-8">
|
||||||
|
<PetDisplay pet={pet} />
|
||||||
|
<InteractionMenu
|
||||||
|
pet={pet}
|
||||||
|
onFeed={handleFeed}
|
||||||
|
onPlay={handlePlay}
|
||||||
|
onSleep={handleSleep}
|
||||||
|
onCustomize={handleCustomize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
40
src/components/ClassSelection.tsx
Executable file
40
src/components/ClassSelection.tsx
Executable file
@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PET_CLASSES } from '../data/petClasses';
|
||||||
|
import { PetClassInfo } from '../types/Pet';
|
||||||
|
|
||||||
|
interface ClassSelectionProps {
|
||||||
|
onSelect: (classKey: string, classInfo: PetClassInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ClassSelection({ onSelect }: ClassSelectionProps) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 text-white p-8">
|
||||||
|
<h1 className="text-4xl font-bold text-center mb-12">Choose Your Virtual Companion</h1>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-7xl mx-auto">
|
||||||
|
{Object.entries(PET_CLASSES).map(([key, classInfo]) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
onClick={() => onSelect(key, classInfo)}
|
||||||
|
className={`bg-${classInfo.color}-900/30 hover:bg-${classInfo.color}-800/50
|
||||||
|
border-2 border-${classInfo.color}-500/50 rounded-lg p-6
|
||||||
|
transition-all duration-300 transform hover:scale-105`}
|
||||||
|
>
|
||||||
|
<div className="text-4xl mb-4">{classInfo.emoji}</div>
|
||||||
|
<h3 className={`text-2xl font-bold text-${classInfo.color}-400 mb-2`}>
|
||||||
|
{classInfo.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-300 mb-4">{classInfo.description}</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{classInfo.modifiers.map((modifier, index) => (
|
||||||
|
<div key={index} className="text-sm text-gray-400 flex items-center">
|
||||||
|
<span className="mr-2">•</span>
|
||||||
|
{modifier}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
64
src/components/GatherResourcesButton.tsx
Executable file
64
src/components/GatherResourcesButton.tsx
Executable file
@ -0,0 +1,64 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { FeatherIcon as GatherIcon } from 'lucide-react';
|
||||||
|
import ResourceSelectionModal from './ResourceSelectionModal';
|
||||||
|
import { Pet } from '../types/Pet';
|
||||||
|
import { updatePetAction } from '../services/api/api';
|
||||||
|
import { PetAction } from '../types/PetUpdateActionRequest';
|
||||||
|
|
||||||
|
interface GatherResourcesButtonProps {
|
||||||
|
pet: Pet;
|
||||||
|
onGatherStart: () => void;
|
||||||
|
onGatherComplete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceToActionMap: Record<string, PetAction> = {
|
||||||
|
wisdom: 'GATHERING_WISDOM',
|
||||||
|
gold: 'GATHERING_GOLD',
|
||||||
|
food: 'GATHERING_FOOD'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function GatherResourcesButton({ pet, onGatherStart, onGatherComplete }: GatherResourcesButtonProps) {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [isGathering, setIsGathering] = useState(false);
|
||||||
|
|
||||||
|
const handleGatherStart = async (resourceType: string) => {
|
||||||
|
setIsGathering(true);
|
||||||
|
onGatherStart();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const petAction = resourceToActionMap[resourceType];
|
||||||
|
await updatePetAction(pet.id, { petAction });
|
||||||
|
onGatherComplete();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to gather resources:', error);
|
||||||
|
} finally {
|
||||||
|
setIsGathering(false);
|
||||||
|
setIsModalOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
disabled={isGathering}
|
||||||
|
className={`flex items-center justify-center space-x-2
|
||||||
|
bg-amber-900/30 hover:bg-amber-800/50
|
||||||
|
border-2 border-amber-500/50 rounded-lg p-4
|
||||||
|
transition-all duration-300 transform hover:scale-105 w-full
|
||||||
|
${isGathering ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
|
>
|
||||||
|
<GatherIcon className="w-6 h-6" />
|
||||||
|
<span>{isGathering ? 'Gathering...' : 'Gather Resources'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ResourceSelectionModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
onGather={handleGatherStart}
|
||||||
|
pet={pet}
|
||||||
|
isGathering={isGathering}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
42
src/components/InteractionMenu.tsx
Executable file
42
src/components/InteractionMenu.tsx
Executable file
@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Pizza, PlayCircle, Moon, Paintbrush } from 'lucide-react';
|
||||||
|
import GatherResourcesButton from './GatherResourcesButton';
|
||||||
|
import { Pet } from '../types/Pet';
|
||||||
|
|
||||||
|
interface InteractionMenuProps {
|
||||||
|
pet: Pet;
|
||||||
|
onFeed: () => void;
|
||||||
|
onPlay: () => void;
|
||||||
|
onSleep: () => void;
|
||||||
|
onCustomize: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function InteractionMenu({ pet, onFeed, onPlay, onSleep, onCustomize }: InteractionMenuProps) {
|
||||||
|
const ActionButton = ({ icon: Icon, label, onClick, color }: { icon: any; label: string; onClick: () => void; color: string }) => (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={`flex items-center justify-center space-x-2 bg-${color}-900/30
|
||||||
|
hover:bg-${color}-800/50 border-2 border-${color}-500/50 rounded-lg p-4
|
||||||
|
transition-all duration-300 transform hover:scale-105 w-full`}
|
||||||
|
>
|
||||||
|
<Icon className="w-6 h-6" />
|
||||||
|
<span>{label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
<ActionButton icon={Pizza} label="Feed" onClick={onFeed} color="green" />
|
||||||
|
<ActionButton icon={PlayCircle} label="Play" onClick={onPlay} color="blue" />
|
||||||
|
<ActionButton icon={Moon} label="Sleep" onClick={onSleep} color="purple" />
|
||||||
|
<ActionButton icon={Paintbrush} label="Customize" onClick={onCustomize} color="pink" />
|
||||||
|
<div className="col-span-2 md:col-span-3">
|
||||||
|
<GatherResourcesButton
|
||||||
|
pet={pet}
|
||||||
|
onGatherStart={() => console.log('Gathering started')}
|
||||||
|
onGatherComplete={() => console.log('Gathering completed')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
56
src/components/PetDisplay.tsx
Executable file
56
src/components/PetDisplay.tsx
Executable file
@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Pet } from '../types/Pet';
|
||||||
|
import { Brain, Dumbbell, Heart, Sparkles, Coins, Pizza, Trash2 } from 'lucide-react';
|
||||||
|
import { PET_CLASSES } from '../data/petClasses';
|
||||||
|
|
||||||
|
interface PetDisplayProps {
|
||||||
|
pet: Pet;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PetDisplay({ pet }: PetDisplayProps) {
|
||||||
|
const StatBar = ({ value, label, icon: Icon }: { value: number; label: string; icon: any }) => (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Icon className="w-5 h-5" />
|
||||||
|
<div className="flex-1 bg-gray-700 rounded-full h-4">
|
||||||
|
<div
|
||||||
|
className="bg-blue-500 rounded-full h-4"
|
||||||
|
style={{ width: `${value}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm">{value}%</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ResourceCounter = ({ value, label, icon: Icon }: { value: number; label: string; icon: any }) => (
|
||||||
|
<div className="flex items-center space-x-2 bg-gray-800 rounded-lg p-2">
|
||||||
|
<Icon className="w-5 h-5" />
|
||||||
|
<span>{label}:</span>
|
||||||
|
<span className="font-bold">{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-900 p-6 rounded-xl">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<div className="text-8xl mb-4">{PET_CLASSES[pet.class].emoji}</div>
|
||||||
|
<h2 className="text-2xl font-bold">{pet.name}</h2>
|
||||||
|
<p className={`text-${PET_CLASSES[pet.class].color}-400`}>
|
||||||
|
{PET_CLASSES[pet.class].name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 mb-6">
|
||||||
|
<StatBar value={pet.stats.intelligence} label="Intelligence" icon={Brain} />
|
||||||
|
<StatBar value={pet.stats.strength} label="Strength" icon={Dumbbell} />
|
||||||
|
<StatBar value={pet.stats.charisma} label="Charisma" icon={Heart} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<ResourceCounter value={pet.resources.wisdom} label="Wisdom" icon={Sparkles} />
|
||||||
|
<ResourceCounter value={pet.resources.gold} label="Gold" icon={Coins} />
|
||||||
|
<ResourceCounter value={pet.resources.food} label="Food" icon={Pizza} />
|
||||||
|
<ResourceCounter value={pet.resources.junk} label="Junk" icon={Trash2} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
121
src/components/ResourceSelectionModal.tsx
Executable file
121
src/components/ResourceSelectionModal.tsx
Executable file
@ -0,0 +1,121 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Brain, Coins, Pizza, X, Loader2 } from 'lucide-react';
|
||||||
|
import { Pet } from '../types/Pet';
|
||||||
|
|
||||||
|
interface ResourceSelectionModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onGather: (resourceType: string) => void;
|
||||||
|
pet: Pet;
|
||||||
|
isGathering: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResourceOption {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
icon: React.ElementType;
|
||||||
|
formula: string;
|
||||||
|
stat: keyof typeof statMultipliers;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statMultipliers = {
|
||||||
|
intelligence: 2,
|
||||||
|
strength: 3,
|
||||||
|
charisma: 1.5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const resourceOptions: ResourceOption[] = [
|
||||||
|
{
|
||||||
|
type: 'wisdom',
|
||||||
|
name: 'Wisdom',
|
||||||
|
icon: Brain,
|
||||||
|
formula: 'Intelligence × 2',
|
||||||
|
stat: 'intelligence',
|
||||||
|
color: 'purple',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'gold',
|
||||||
|
name: 'Gold',
|
||||||
|
icon: Coins,
|
||||||
|
formula: 'Strength × 3',
|
||||||
|
stat: 'strength',
|
||||||
|
color: 'yellow',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'food',
|
||||||
|
name: 'Food',
|
||||||
|
icon: Pizza,
|
||||||
|
formula: 'Charisma × 1.5',
|
||||||
|
stat: 'charisma',
|
||||||
|
color: 'green',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ResourceSelectionModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onGather,
|
||||||
|
pet,
|
||||||
|
isGathering,
|
||||||
|
}: ResourceSelectionModalProps) {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const calculateEstimatedYield = (option: ResourceOption) => {
|
||||||
|
const baseStat = pet.stats[option.stat];
|
||||||
|
const multiplier = statMultipliers[option.stat];
|
||||||
|
return Math.floor(baseStat * multiplier);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||||
|
<div className="bg-gray-800 rounded-xl max-w-2xl w-full p-6 relative">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-4 right-4 text-gray-400 hover:text-white"
|
||||||
|
disabled={isGathering}
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h2 className="text-2xl font-bold mb-6 text-white">Gather Resources</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{resourceOptions.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option.type}
|
||||||
|
onClick={() => onGather(option.type)}
|
||||||
|
disabled={isGathering}
|
||||||
|
className={`bg-gray-700/50 hover:bg-gray-700
|
||||||
|
border-2 border-${option.color}-500/30
|
||||||
|
rounded-lg p-4 transition-all duration-300
|
||||||
|
transform hover:scale-105 text-left
|
||||||
|
${isGathering ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2 mb-2">
|
||||||
|
<option.icon className={`w-6 h-6 text-${option.color}-400`} />
|
||||||
|
<span className="font-semibold text-white">{option.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-400 mb-2">
|
||||||
|
Formula: {option.formula}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
Estimated yield per hour:
|
||||||
|
<span className={`ml-2 font-bold text-${option.color}-400`}>
|
||||||
|
{calculateEstimatedYield(option)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isGathering && (
|
||||||
|
<div className="mt-4 flex items-center justify-center text-blue-400">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin mr-2" />
|
||||||
|
<span>Gathering resources...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
81
src/data/petClasses.ts
Executable file
81
src/data/petClasses.ts
Executable file
@ -0,0 +1,81 @@
|
|||||||
|
import { PetClassInfo } from '../types/Pet';
|
||||||
|
|
||||||
|
export const PET_CLASSES: Record<string, PetClassInfo> = {
|
||||||
|
FOREST_SPIRIT: {
|
||||||
|
name: 'Forest Spirit',
|
||||||
|
description: 'A mystical guardian of nature, attuned to the forest\'s wisdom.',
|
||||||
|
modifiers: [
|
||||||
|
'-20% food consumption',
|
||||||
|
'+15% wisdom generation',
|
||||||
|
'Natural healing ability'
|
||||||
|
],
|
||||||
|
color: 'emerald',
|
||||||
|
emoji: '🌿'
|
||||||
|
},
|
||||||
|
OCEAN_GUARDIAN: {
|
||||||
|
name: 'Ocean Guardian',
|
||||||
|
description: 'A majestic creature of the deep, master of the seas.',
|
||||||
|
modifiers: [
|
||||||
|
'+20% resource gathering',
|
||||||
|
'Water breathing',
|
||||||
|
'Enhanced mobility'
|
||||||
|
],
|
||||||
|
color: 'cyan',
|
||||||
|
emoji: '🌊'
|
||||||
|
},
|
||||||
|
FIRE_ELEMENTAL: {
|
||||||
|
name: 'Fire Elemental',
|
||||||
|
description: 'A passionate spirit of flame, burning with inner strength.',
|
||||||
|
modifiers: [
|
||||||
|
'+25% strength gains',
|
||||||
|
'Fire resistance',
|
||||||
|
'Heat generation'
|
||||||
|
],
|
||||||
|
color: 'red',
|
||||||
|
emoji: '🔥'
|
||||||
|
},
|
||||||
|
MYTHICAL_BEAST: {
|
||||||
|
name: 'Mythical Beast',
|
||||||
|
description: 'A legendary creature of ancient lore.',
|
||||||
|
modifiers: [
|
||||||
|
'+30% charisma',
|
||||||
|
'Rare item finding',
|
||||||
|
'Enhanced abilities at night'
|
||||||
|
],
|
||||||
|
color: 'purple',
|
||||||
|
emoji: '🦄'
|
||||||
|
},
|
||||||
|
SHADOW_WALKER: {
|
||||||
|
name: 'Shadow Walker',
|
||||||
|
description: 'A mysterious being that moves between light and darkness.',
|
||||||
|
modifiers: [
|
||||||
|
'Stealth abilities',
|
||||||
|
'+25% junk value',
|
||||||
|
'Night vision'
|
||||||
|
],
|
||||||
|
color: 'violet',
|
||||||
|
emoji: '👻'
|
||||||
|
},
|
||||||
|
CYBER_PET: {
|
||||||
|
name: 'Cyber Pet',
|
||||||
|
description: 'A digital companion from the future.',
|
||||||
|
modifiers: [
|
||||||
|
'+30% intelligence growth',
|
||||||
|
'Digital interface',
|
||||||
|
'Quick processing'
|
||||||
|
],
|
||||||
|
color: 'blue',
|
||||||
|
emoji: '🤖'
|
||||||
|
},
|
||||||
|
BIO_MECHANICAL: {
|
||||||
|
name: 'Bio-Mechanical Hybrid',
|
||||||
|
description: 'A perfect fusion of organic life and advanced technology.',
|
||||||
|
modifiers: [
|
||||||
|
'Self-repair capability',
|
||||||
|
'+20% all stats',
|
||||||
|
'Resource optimization'
|
||||||
|
],
|
||||||
|
color: 'teal',
|
||||||
|
emoji: '🦾'
|
||||||
|
}
|
||||||
|
};
|
3
src/index.css
Executable file
3
src/index.css
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
10
src/main.tsx
Executable file
10
src/main.tsx
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
37
src/services/api/api.ts
Normal file
37
src/services/api/api.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { ApiService } from './index';
|
||||||
|
import { Pet } from '../../types/Pet';
|
||||||
|
import { PetCreationRequest } from '../../types/PetCreationRequest';
|
||||||
|
import { PetUpdateActionRequest } from '../../types/PetUpdateActionRequest';
|
||||||
|
|
||||||
|
// Get API service instance
|
||||||
|
const api = ApiService.getInstance();
|
||||||
|
|
||||||
|
export async function fetchPets(): Promise<Pet[]> {
|
||||||
|
try {
|
||||||
|
const response = await api.get<Pet[]>('/api/v1/pet');
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to fetch pets:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPet(data: PetCreationRequest): Promise<Pet> {
|
||||||
|
try {
|
||||||
|
const response = await api.post<Pet>('/api/v1/pet', data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to create pet:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePetAction(petId: string, data: PetUpdateActionRequest): Promise<Pet> {
|
||||||
|
try {
|
||||||
|
const response = await api.put<Pet>(`/api/v1/pet/${petId}/action`, data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to update pet action:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
14
src/services/api/config.ts
Executable file
14
src/services/api/config.ts
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
import { ApiConfig } from './types';
|
||||||
|
|
||||||
|
// API configuration
|
||||||
|
export const apiConfig: ApiConfig = {
|
||||||
|
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5278',
|
||||||
|
timeout: 10000, // 10 seconds
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
||||||
|
},
|
||||||
|
};
|
52
src/services/api/error.ts
Executable file
52
src/services/api/error.ts
Executable file
@ -0,0 +1,52 @@
|
|||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ApiError } from './types';
|
||||||
|
|
||||||
|
export class ApiErrorHandler {
|
||||||
|
static handle(error: AxiosError): ApiError {
|
||||||
|
if (error.response) {
|
||||||
|
// Server responded with a status code outside of 2xx range
|
||||||
|
return {
|
||||||
|
message: error.response.data?.message || 'Server error occurred',
|
||||||
|
code: 'SERVER_ERROR',
|
||||||
|
status: error.response.status,
|
||||||
|
details: error.response.data,
|
||||||
|
};
|
||||||
|
} else if (error.request) {
|
||||||
|
// Request was made but no response received
|
||||||
|
return {
|
||||||
|
message: 'No response received from server',
|
||||||
|
code: 'NETWORK_ERROR',
|
||||||
|
status: 0,
|
||||||
|
details: {
|
||||||
|
request: error.request,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Error occurred while setting up the request
|
||||||
|
return {
|
||||||
|
message: error.message || 'An error occurred while making the request',
|
||||||
|
code: 'REQUEST_SETUP_ERROR',
|
||||||
|
status: 0,
|
||||||
|
details: {
|
||||||
|
config: error.config,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static isNetworkError(error: ApiError): boolean {
|
||||||
|
return error.code === 'NETWORK_ERROR';
|
||||||
|
}
|
||||||
|
|
||||||
|
static isTimeoutError(error: ApiError): boolean {
|
||||||
|
return error.message.toLowerCase().includes('timeout');
|
||||||
|
}
|
||||||
|
|
||||||
|
static isServerError(error: ApiError): boolean {
|
||||||
|
return error.status >= 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
static isClientError(error: ApiError): boolean {
|
||||||
|
return error.status >= 400 && error.status < 500;
|
||||||
|
}
|
||||||
|
}
|
45
src/services/api/examples.ts
Executable file
45
src/services/api/examples.ts
Executable file
@ -0,0 +1,45 @@
|
|||||||
|
// Example usage of the API service
|
||||||
|
import { ApiService } from './index';
|
||||||
|
import { Pet } from '../../types/Pet';
|
||||||
|
|
||||||
|
// Get API service instance
|
||||||
|
const api = ApiService.getInstance();
|
||||||
|
|
||||||
|
// Example functions using the API service
|
||||||
|
export async function getPet(id: string) {
|
||||||
|
try {
|
||||||
|
const response = await api.get<Pet>(`/pets/${id}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (api.isNetworkError(error)) {
|
||||||
|
console.error('Network error occurred');
|
||||||
|
} else if (api.isTimeoutError(error)) {
|
||||||
|
console.error('Request timed out');
|
||||||
|
} else if (api.isServerError(error)) {
|
||||||
|
console.error('Server error occurred');
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePet(id: string, data: Partial<Pet>) {
|
||||||
|
try {
|
||||||
|
const response = await api.put<Pet>(`/pets/${id}`, data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to update pet:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function gatherResources(id: string, resourceType: string) {
|
||||||
|
try {
|
||||||
|
const response = await api.post<{ success: boolean }>(`/pets/${id}/gather`, {
|
||||||
|
resourceType,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to gather resources:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
90
src/services/api/index.ts
Executable file
90
src/services/api/index.ts
Executable file
@ -0,0 +1,90 @@
|
|||||||
|
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||||
|
import { apiConfig } from './config';
|
||||||
|
import { setupInterceptors } from './interceptors';
|
||||||
|
import { ApiErrorHandler } from './error';
|
||||||
|
import { ApiResponse, RequestOptions, ApiError } from './types';
|
||||||
|
|
||||||
|
class ApiService {
|
||||||
|
private static instance: ApiService;
|
||||||
|
private axios: AxiosInstance;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
// Create axios instance with base configuration
|
||||||
|
this.axios = axios.create(apiConfig);
|
||||||
|
|
||||||
|
// Setup interceptors
|
||||||
|
setupInterceptors(this.axios);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): ApiService {
|
||||||
|
if (!ApiService.instance) {
|
||||||
|
ApiService.instance = new ApiService();
|
||||||
|
}
|
||||||
|
return ApiService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T>(
|
||||||
|
method: string,
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
options: RequestOptions = {}
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
try {
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
config.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.axios.request<T>(config);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: response.data,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: response.headers as Record<string, string>,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw ApiErrorHandler.handle(error as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get<T>(url: string, options?: RequestOptions): Promise<ApiResponse<T>> {
|
||||||
|
return this.request<T>('GET', url, undefined, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async post<T>(url: string, data?: any, options?: RequestOptions): Promise<ApiResponse<T>> {
|
||||||
|
return this.request<T>('POST', url, data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async put<T>(url: string, data?: any, options?: RequestOptions): Promise<ApiResponse<T>> {
|
||||||
|
return this.request<T>('PUT', url, data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async delete<T>(url: string, options?: RequestOptions): Promise<ApiResponse<T>> {
|
||||||
|
return this.request<T>('DELETE', url, undefined, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility method to check if an error is a specific type
|
||||||
|
public isNetworkError(error: ApiError): boolean {
|
||||||
|
return ApiErrorHandler.isNetworkError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isTimeoutError(error: ApiError): boolean {
|
||||||
|
return ApiErrorHandler.isTimeoutError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isServerError(error: ApiError): boolean {
|
||||||
|
return ApiErrorHandler.isServerError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isClientError(error: ApiError): boolean {
|
||||||
|
return ApiErrorHandler.isClientError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ApiService }
|
66
src/services/api/interceptors.ts
Executable file
66
src/services/api/interceptors.ts
Executable file
@ -0,0 +1,66 @@
|
|||||||
|
import { AxiosInstance } from 'axios';
|
||||||
|
import { ApiError } from './types';
|
||||||
|
|
||||||
|
export function setupInterceptors(axiosInstance: AxiosInstance) {
|
||||||
|
// Request interceptor
|
||||||
|
axiosInstance.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// Get token from storage
|
||||||
|
const token = localStorage.getItem('auth_token');
|
||||||
|
|
||||||
|
// Add authorization header if token exists
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log request (development only)
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log(`🚀 [API] ${config.method?.toUpperCase()} ${config.url}`, {
|
||||||
|
data: config.data,
|
||||||
|
params: config.params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// Log request error (development only)
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.error('❌ [API] Request Error:', error);
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Response interceptor
|
||||||
|
axiosInstance.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
// Log response (development only)
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log(`✅ [API] Response:`, {
|
||||||
|
status: response.status,
|
||||||
|
data: response.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// Log response error (development only)
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.error('❌ [API] Response Error:', error.response || error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle authentication errors
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
// Clear token and redirect to login
|
||||||
|
localStorage.removeItem('auth_token');
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return axiosInstance;
|
||||||
|
}
|
32
src/services/api/types.ts
Executable file
32
src/services/api/types.ts
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
// HTTP Methods supported by the API
|
||||||
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||||
|
|
||||||
|
// Standard API Response interface
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
data: T;
|
||||||
|
status: number;
|
||||||
|
statusText: string;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard API Error interface
|
||||||
|
export interface ApiError {
|
||||||
|
message: string;
|
||||||
|
code: string;
|
||||||
|
status: number;
|
||||||
|
details?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Configuration interface
|
||||||
|
export interface ApiConfig {
|
||||||
|
baseURL: string;
|
||||||
|
timeout: number;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request options interface
|
||||||
|
export interface RequestOptions {
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
params?: Record<string, any>;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
31
src/types/Pet.ts
Executable file
31
src/types/Pet.ts
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
export type PetClass = 'FOREST_SPIRIT' | 'OCEAN_GUARDIAN' | 'FIRE_ELEMENTAL' | 'MYTHICAL_BEAST' | 'SHADOW_WALKER' | 'CYBER_PET' | 'BIO_MECHANICAL';
|
||||||
|
|
||||||
|
export interface PetStats {
|
||||||
|
intelligence: number;
|
||||||
|
strength: number;
|
||||||
|
charisma: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Resources {
|
||||||
|
wisdom: number;
|
||||||
|
gold: number;
|
||||||
|
food: number;
|
||||||
|
junk: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Pet {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
class: PetClass;
|
||||||
|
stats: PetStats;
|
||||||
|
resources: Resources;
|
||||||
|
level: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PetClassInfo {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
modifiers: string[];
|
||||||
|
color: string;
|
||||||
|
emoji: string;
|
||||||
|
}
|
6
src/types/PetCreationRequest.ts
Normal file
6
src/types/PetCreationRequest.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { PetClass } from "./Pet";
|
||||||
|
|
||||||
|
export interface PetCrationRequest {
|
||||||
|
name: string;
|
||||||
|
class: PetClass;
|
||||||
|
}
|
5
src/types/PetUpdateActionRequest.ts
Normal file
5
src/types/PetUpdateActionRequest.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export type PetAction = 'IDLE' | 'GATHERING_WISDOM' | 'GATHERING_GOLD' | 'GATHERING_FOOD';
|
||||||
|
|
||||||
|
export interface PetUpdateActionRequest {
|
||||||
|
petAction: PetAction;
|
||||||
|
}
|
1
src/vite-env.d.ts
vendored
Executable file
1
src/vite-env.d.ts
vendored
Executable file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
8
tailwind.config.js
Executable file
8
tailwind.config.js
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
24
tsconfig.app.json
Executable file
24
tsconfig.app.json
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
7
tsconfig.json
Executable file
7
tsconfig.json
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
22
tsconfig.node.json
Executable file
22
tsconfig.node.json
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
10
vite.config.ts
Executable file
10
vite.config.ts
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ['lucide-react'],
|
||||||
|
},
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user