feat: integrate Firebase for authentication and data management, add GameItem type, and enhance UI with tooltips
This commit is contained in:
parent
62634a426e
commit
a12cfc5a2a
7
.env.example
Normal file
7
.env.example
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
VITE_FIREBASE_API_KEY=your-api-key
|
||||||
|
VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
|
||||||
|
VITE_FIREBASE_PROJECT_ID=your-project-id
|
||||||
|
VITE_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
|
||||||
|
VITE_FIREBASE_MESSAGING_SENDER_ID=your-sender-id
|
||||||
|
VITE_FIREBASE_APP_ID=your-app-id
|
||||||
|
VITE_FIREBASE_MEASUREMENT_ID=your-measurement-id
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -22,3 +22,5 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
.env
|
||||||
|
1071
package-lock.json
generated
1071
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
|
"firebase": "^11.3.1",
|
||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
@ -31,4 +32,4 @@
|
|||||||
"typescript-eslint": "^8.3.0",
|
"typescript-eslint": "^8.3.0",
|
||||||
"vite": "^5.4.2"
|
"vite": "^5.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
57
src/App.tsx
57
src/App.tsx
@ -1,29 +1,45 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { onAuthStateChanged } from 'firebase/auth';
|
||||||
|
import { auth } from './config/firebase';
|
||||||
import PetDisplay from './components/PetDisplay';
|
import PetDisplay from './components/PetDisplay';
|
||||||
import InteractionMenu from './components/InteractionMenu';
|
import InteractionMenu from './components/InteractionMenu';
|
||||||
import PetRegister from './components/PetRegister';
|
import PetRegister from './components/PetRegister';
|
||||||
import AnimatedBackground from './components/AnimatedBackground';
|
import AnimatedBackground from './components/AnimatedBackground';
|
||||||
|
import AuthForm from './components/auth/AuthForm';
|
||||||
import { Pet } from './types/Pet';
|
import { Pet } from './types/Pet';
|
||||||
import { fetchPets } from './services/api/api';
|
import { fetchPets } from './services/api/api';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [pet, setPet] = useState<Pet | null>(null);
|
const [pet, setPet] = useState<Pet | null>(null);
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadPets = async () => {
|
const unsubscribe = onAuthStateChanged(auth, (user) => {
|
||||||
try {
|
setIsAuthenticated(!!user);
|
||||||
const pets = await fetchPets();
|
setIsLoading(false);
|
||||||
if (pets.length > 0) {
|
});
|
||||||
setPet(pets[0]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading pets:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadPets();
|
return () => unsubscribe();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
const loadPets = async () => {
|
||||||
|
try {
|
||||||
|
const pets = await fetchPets();
|
||||||
|
if (pets.length > 0) {
|
||||||
|
setPet(pets[0]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading pets:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPets();
|
||||||
|
}
|
||||||
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
const handleCustomize = () => {
|
const handleCustomize = () => {
|
||||||
console.log('Customize pet');
|
console.log('Customize pet');
|
||||||
};
|
};
|
||||||
@ -32,6 +48,25 @@ export default function App() {
|
|||||||
setPet(updatedPet);
|
setPet(updatedPet);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
|
||||||
|
<div className="text-white">Loading...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AnimatedBackground />
|
||||||
|
<div className="relative z-10">
|
||||||
|
<AuthForm onAuthSuccess={() => setIsAuthenticated(true)} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!pet) {
|
if (!pet) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
import { Pet } from '../types/Pet';
|
import { Pet } from '../types/Pet';
|
||||||
|
import Tooltip from './Tooltip';
|
||||||
import { isActionActive, formatResourceName, getResourceFromAction } from '../utils/petUtils';
|
import { isActionActive, formatResourceName, getResourceFromAction } from '../utils/petUtils';
|
||||||
|
|
||||||
const colorClassMap = {
|
const colorClassMap = {
|
||||||
@ -29,6 +30,15 @@ const getActionVerb = (actionType: 'gather' | 'explore' | 'battle'): string => {
|
|||||||
return verbs[actionType];
|
return verbs[actionType];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getActionDescription = (actionType: 'gather' | 'explore' | 'battle'): string => {
|
||||||
|
const descriptions = {
|
||||||
|
gather: 'Send your pet to gather resources<br/>Your pet will collect items over time',
|
||||||
|
explore: 'Send your pet to explore<br/>Your pet might find rare items',
|
||||||
|
battle: 'Send your pet to battle<br/>Your pet will gain experience and items',
|
||||||
|
};
|
||||||
|
return descriptions[actionType];
|
||||||
|
};
|
||||||
|
|
||||||
type ButtonColor = keyof typeof colorClassMap;
|
type ButtonColor = keyof typeof colorClassMap;
|
||||||
|
|
||||||
interface ActionResourceButtonProps {
|
interface ActionResourceButtonProps {
|
||||||
@ -37,6 +47,7 @@ interface ActionResourceButtonProps {
|
|||||||
label: string;
|
label: string;
|
||||||
actionType: 'gather' | 'explore' | 'battle';
|
actionType: 'gather' | 'explore' | 'battle';
|
||||||
color: ButtonColor;
|
color: ButtonColor;
|
||||||
|
actionText?: string;
|
||||||
onActionClick: () => void;
|
onActionClick: () => void;
|
||||||
onActionComplete: (updatedPet: Pet) => void;
|
onActionComplete: (updatedPet: Pet) => void;
|
||||||
}
|
}
|
||||||
@ -47,6 +58,7 @@ export default function ActionResourceButton({
|
|||||||
label,
|
label,
|
||||||
actionType,
|
actionType,
|
||||||
color,
|
color,
|
||||||
|
actionText,
|
||||||
onActionClick
|
onActionClick
|
||||||
}: ActionResourceButtonProps) {
|
}: ActionResourceButtonProps) {
|
||||||
const isActive = isActionActive(pet.petGatherAction, actionType);
|
const isActive = isActionActive(pet.petGatherAction, actionType);
|
||||||
@ -67,7 +79,7 @@ export default function ActionResourceButton({
|
|||||||
return label;
|
return label;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const button = (
|
||||||
<button
|
<button
|
||||||
onClick={onActionClick}
|
onClick={onActionClick}
|
||||||
className={`flex items-center justify-center space-x-2
|
className={`flex items-center justify-center space-x-2
|
||||||
@ -79,4 +91,10 @@ export default function ActionResourceButton({
|
|||||||
<span>{getButtonText()}</span>
|
<span>{getButtonText()}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content={actionText || ''}>
|
||||||
|
{button}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -143,6 +143,7 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
|||||||
onClick={performBasicAction('FEED')}
|
onClick={performBasicAction('FEED')}
|
||||||
color="green"
|
color="green"
|
||||||
disabled={remainingCooldown !== null}
|
disabled={remainingCooldown !== null}
|
||||||
|
actionText="Feed your pet<br/>+1 Strengh (up to max)<br/>+5 Health<br/>-1 Food"
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={PlayCircle}
|
icon={PlayCircle}
|
||||||
@ -150,6 +151,7 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
|||||||
onClick={performBasicAction('PLAY')}
|
onClick={performBasicAction('PLAY')}
|
||||||
color="blue"
|
color="blue"
|
||||||
disabled={remainingCooldown !== null}
|
disabled={remainingCooldown !== null}
|
||||||
|
actionText="Play with your pet<br/>+1 Charisma (up to max)<br/>-1 Junk"
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={Moon}
|
icon={Moon}
|
||||||
@ -157,6 +159,7 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
|||||||
onClick={performBasicAction('SLEEP')}
|
onClick={performBasicAction('SLEEP')}
|
||||||
color="purple"
|
color="purple"
|
||||||
disabled={remainingCooldown !== null}
|
disabled={remainingCooldown !== null}
|
||||||
|
actionText="Put your pet to sleep<br/>+1 Intelligence (up to max)<br/>+1 Strengh (up to max)<br/>+15 Health"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="col-span-2 md:col-span-3 grid grid-cols-3 gap-4">
|
<div className="col-span-2 md:col-span-3 grid grid-cols-3 gap-4">
|
||||||
@ -168,6 +171,7 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
|||||||
color="amber"
|
color="amber"
|
||||||
onActionClick={() => handleActionStart('gather')}
|
onActionClick={() => handleActionStart('gather')}
|
||||||
onActionComplete={handleGatherComplete}
|
onActionComplete={handleGatherComplete}
|
||||||
|
actionText='Send your pet to gather resources<br/>Your pet will collect items over time'
|
||||||
/>
|
/>
|
||||||
<ActionResourceButton
|
<ActionResourceButton
|
||||||
pet={pet}
|
pet={pet}
|
||||||
@ -177,6 +181,7 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
|||||||
color="emerald"
|
color="emerald"
|
||||||
onActionClick={() => handleActionStart('explore')}
|
onActionClick={() => handleActionStart('explore')}
|
||||||
onActionComplete={handleGatherComplete}
|
onActionComplete={handleGatherComplete}
|
||||||
|
actionText='Send your pet to explore based on their strength<br/>Your pet might find wisdom and rare items but also may lose health'
|
||||||
/>
|
/>
|
||||||
<ActionResourceButton
|
<ActionResourceButton
|
||||||
pet={pet}
|
pet={pet}
|
||||||
@ -186,6 +191,7 @@ export default function InteractionMenu({ pet, onPetUpdate }: InteractionMenuPro
|
|||||||
color="red"
|
color="red"
|
||||||
onActionClick={() => handleActionStart('battle')}
|
onActionClick={() => handleActionStart('battle')}
|
||||||
onActionComplete={handleGatherComplete}
|
onActionComplete={handleGatherComplete}
|
||||||
|
actionText='Send your pet to battle based on their strength<br/>Your pet will gain experience and items, but also may lose health'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
20
src/components/Tooltip.tsx
Normal file
20
src/components/Tooltip.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
interface TooltipProps {
|
||||||
|
content: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Tooltip({ content, children }: TooltipProps) {
|
||||||
|
return (
|
||||||
|
<div className="group relative inline-block w-full">
|
||||||
|
{children}
|
||||||
|
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300
|
||||||
|
absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2
|
||||||
|
bg-gray-900 text-white text-sm rounded-lg shadow-lg whitespace-nowrap
|
||||||
|
border border-gray-700 pointer-events-none z-50">
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: content }} />
|
||||||
|
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2
|
||||||
|
border-4 border-transparent border-t-gray-900" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
129
src/components/auth/AuthForm.tsx
Normal file
129
src/components/auth/AuthForm.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { login, register, signInWithGoogle } from '../../services/auth/auth';
|
||||||
|
|
||||||
|
interface AuthFormProps {
|
||||||
|
onAuthSuccess: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AuthForm({ onAuthSuccess }: AuthFormProps) {
|
||||||
|
const [isLogin, setIsLogin] = useState(true);
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isLogin) {
|
||||||
|
await login(email, password);
|
||||||
|
} else {
|
||||||
|
await register(email, password);
|
||||||
|
}
|
||||||
|
onAuthSuccess();
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoogleSignIn = async () => {
|
||||||
|
try {
|
||||||
|
await signInWithGoogle();
|
||||||
|
onAuthSuccess();
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-900">
|
||||||
|
<div className="bg-gray-800 p-8 rounded-xl shadow-xl w-96">
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-6 text-center">
|
||||||
|
{isLogin ? 'Login' : 'Register'}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-500 text-white p-3 rounded mb-4 text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="w-full p-3 rounded bg-gray-700 text-white"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="w-full p-3 rounded bg-gray-700 text-white"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white p-3 rounded"
|
||||||
|
>
|
||||||
|
{isLogin ? 'Login' : 'Register'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-4 relative">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-gray-600"></div>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-sm">
|
||||||
|
<span className="px-2 bg-gray-800 text-gray-400">Or continue with</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleGoogleSignIn}
|
||||||
|
className="mt-4 w-full flex items-center justify-center gap-2 bg-white hover:bg-gray-100 text-gray-900 font-semibold p-3 rounded"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||||
|
fill="#4285F4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||||
|
fill="#34A853"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||||
|
fill="#FBBC05"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||||
|
fill="#EA4335"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Continue with Google
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setIsLogin(!isLogin)}
|
||||||
|
className="w-full text-blue-400 mt-4 text-sm"
|
||||||
|
>
|
||||||
|
{isLogin ? 'Need an account? Register' : 'Have an account? Login'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
import Tooltip from '../Tooltip';
|
||||||
|
|
||||||
interface ActionButtonProps {
|
interface ActionButtonProps {
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
@ -6,10 +7,11 @@ interface ActionButtonProps {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
color: string;
|
color: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
actionText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ActionButton({ icon: Icon, label, onClick, color, disabled }: ActionButtonProps) {
|
export default function ActionButton({ icon: Icon, label, onClick, color, disabled, actionText }: ActionButtonProps) {
|
||||||
return (
|
const button = (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -22,4 +24,14 @@ export default function ActionButton({ icon: Icon, label, onClick, color, disabl
|
|||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!actionText) {
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content={actionText}>
|
||||||
|
{button}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Inventory, InvItemInteraction, Pet } from '../../types/Pet';
|
import { Inventory, InvItemInteraction, Pet } from '../../types/Pet';
|
||||||
import { putPetItemInteract, getItemIcon } from '../../services/api/api';
|
import { putPetItemInteract, getItemIcon, getItemInfo } from '../../services/api/api';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import { GameItem, ItemRarity } from '../../types/GameItem';
|
||||||
|
|
||||||
interface InventoryModalProps {
|
interface InventoryModalProps {
|
||||||
inventory: Inventory;
|
inventory: Inventory;
|
||||||
@ -17,6 +18,15 @@ export default function InventoryModal({ inventory, petId, onClose, onPetUpdate
|
|||||||
const [selectedItemIndex, setSelectedItemIndex] = useState<number | null>(null);
|
const [selectedItemIndex, setSelectedItemIndex] = useState<number | null>(null);
|
||||||
const [itemIcons, setItemIcons] = useState<Map<number, string>>(new Map());
|
const [itemIcons, setItemIcons] = useState<Map<number, string>>(new Map());
|
||||||
const [loadingIcons, setLoadingIcons] = useState<Set<number>>(new Set());
|
const [loadingIcons, setLoadingIcons] = useState<Set<number>>(new Set());
|
||||||
|
const [selectedItemDetails, setSelectedItemDetails] = useState<GameItem | null>(null);
|
||||||
|
const [loadingDetails, setLoadingDetails] = useState(false);
|
||||||
|
|
||||||
|
const rarityColors = {
|
||||||
|
[ItemRarity.Common]: 'text-gray-200',
|
||||||
|
[ItemRarity.Uncommon]: 'text-green-400',
|
||||||
|
[ItemRarity.Rare]: 'text-blue-400',
|
||||||
|
[ItemRarity.Legendary]: 'text-purple-400'
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchItemIcons = async () => {
|
const fetchItemIcons = async () => {
|
||||||
@ -56,12 +66,26 @@ export default function InventoryModal({ inventory, petId, onClose, onPetUpdate
|
|||||||
};
|
};
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
const handleItemClick = (index: number) => {
|
const handleItemClick = async (index: number) => {
|
||||||
if (selectedItemIndex === index || gridSlots[index] === undefined) {
|
if (selectedItemIndex === index || gridSlots[index] === undefined) {
|
||||||
setSelectedItemIndex(null);
|
setSelectedItemIndex(null);
|
||||||
|
setSelectedItemDetails(null);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
setSelectedItemIndex(index);
|
setSelectedItemIndex(index);
|
||||||
|
const itemId = gridSlots[index];
|
||||||
|
|
||||||
|
if (itemId !== undefined) {
|
||||||
|
setLoadingDetails(true);
|
||||||
|
try {
|
||||||
|
const details = await getItemInfo(itemId);
|
||||||
|
setSelectedItemDetails(details);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load item details:', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingDetails(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -112,6 +136,35 @@ export default function InventoryModal({ inventory, petId, onClose, onPetUpdate
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{selectedItemIndex !== null && (
|
||||||
|
<div className="mt-4 mb-4 p-4 bg-gray-700 rounded">
|
||||||
|
{loadingDetails ? (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : selectedItemDetails ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-lg font-semibold text-white mb-1">
|
||||||
|
{selectedItemDetails.name}
|
||||||
|
</h3>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<span className={rarityColors[selectedItemDetails.rarity]}>
|
||||||
|
{selectedItemDetails.rarity}
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-300">•</span>
|
||||||
|
<span className="text-gray-200">{selectedItemDetails.type}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-300 text-sm space-y-1">
|
||||||
|
{selectedItemDetails.description.split(';').map((line, index) => (
|
||||||
|
<p key={index}>{line.trim()}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-4 grid grid-cols-4 gap-4">
|
<div className="mt-4 grid grid-cols-4 gap-4">
|
||||||
<button
|
<button
|
||||||
disabled={selectedItemIndex === null}
|
disabled={selectedItemIndex === null}
|
||||||
|
19
src/config/firebase.ts
Normal file
19
src/config/firebase.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { initializeApp } from 'firebase/app';
|
||||||
|
import { getFirestore } from 'firebase/firestore';
|
||||||
|
import { getAuth } from 'firebase/auth';
|
||||||
|
import { getAnalytics } from 'firebase/analytics';
|
||||||
|
|
||||||
|
const firebaseConfig = {
|
||||||
|
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
|
||||||
|
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
|
||||||
|
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
|
||||||
|
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
|
||||||
|
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
|
||||||
|
appId: import.meta.env.VITE_FIREBASE_APP_ID,
|
||||||
|
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID,
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = initializeApp(firebaseConfig);
|
||||||
|
export const db = getFirestore(app);
|
||||||
|
export const auth = getAuth(app);
|
||||||
|
export const analytics = getAnalytics(app);
|
@ -3,6 +3,7 @@ import { InvItemInteraction, Pet, Resources } from '../../types/Pet';
|
|||||||
import { PetCreationRequest } from '../../types/PetCreationRequest';
|
import { PetCreationRequest } from '../../types/PetCreationRequest';
|
||||||
import { PetActionGathered, PetUpdateActionRequest } from '../../types/PetAction';
|
import { PetActionGathered, PetUpdateActionRequest } from '../../types/PetAction';
|
||||||
import { PetSkill, Skill } from '../../types/Skills';
|
import { PetSkill, Skill } from '../../types/Skills';
|
||||||
|
import { GameItem } from '../../types/GameItem';
|
||||||
|
|
||||||
// Get API service instance
|
// Get API service instance
|
||||||
const api = ApiService.getInstance();
|
const api = ApiService.getInstance();
|
||||||
@ -52,8 +53,13 @@ export async function putPetItemInteract(petId: string, itemId: number, inter: I
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getItemInfo(itemId: number): Promise<GameItem> {
|
||||||
|
const response = await api.get<GameItem>(`/api/v1/gamedata/item/${itemId}`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getItemIcon(itemId: number): Promise<Blob> {
|
export async function getItemIcon(itemId: number): Promise<Blob> {
|
||||||
const response = await api.get<Blob>(`/api/v1/gamedata/item/icon/${itemId}`, {
|
const response = await api.get<Blob>(`/api/v1/gamedata/item/${itemId}/icon`, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'image/png'
|
Accept: 'image/png'
|
||||||
|
@ -6,14 +6,28 @@ import { ApiResponse, RequestOptions, ApiError } from './types';
|
|||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
private static instance: ApiService;
|
private static instance: ApiService;
|
||||||
private axios: AxiosInstance;
|
private axiosInstance: AxiosInstance;
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
// Create axios instance with base configuration
|
// Create axios instance with base configuration
|
||||||
this.axios = axios.create(apiConfig);
|
this.axiosInstance = axios.create(apiConfig);
|
||||||
|
|
||||||
// Setup interceptors
|
// Setup interceptors
|
||||||
setupInterceptors(this.axios);
|
setupInterceptors(this.axiosInstance);
|
||||||
|
|
||||||
|
// Add request interceptor
|
||||||
|
this.axiosInstance.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const token = localStorage.getItem('TOKEN');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getInstance(): ApiService {
|
public static getInstance(): ApiService {
|
||||||
@ -40,7 +54,7 @@ class ApiService {
|
|||||||
config.data = data;
|
config.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.axios.request<T>(config);
|
const response = await this.axiosInstance.request<T>(config);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: response.data,
|
data: response.data,
|
||||||
|
10
src/services/api/userApi.ts
Normal file
10
src/services/api/userApi.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Pet } from '../../types/Pet';
|
||||||
|
import { ApiService } from './index';
|
||||||
|
|
||||||
|
// Get API service instance
|
||||||
|
const api = ApiService.getInstance();
|
||||||
|
|
||||||
|
export async function fetchPets(): Promise<Pet[]> {
|
||||||
|
const response = await api.get<Pet[]>('/api/v1/pet');
|
||||||
|
return response.data;
|
||||||
|
}
|
40
src/services/auth/auth.ts
Normal file
40
src/services/auth/auth.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
signInWithEmailAndPassword,
|
||||||
|
createUserWithEmailAndPassword,
|
||||||
|
signOut as firebaseSignOut,
|
||||||
|
User,
|
||||||
|
GoogleAuthProvider,
|
||||||
|
signInWithPopup
|
||||||
|
} from 'firebase/auth';
|
||||||
|
import { auth } from '../../config/firebase';
|
||||||
|
|
||||||
|
export async function login(email: string, password: string): Promise<string> {
|
||||||
|
const userCredential = await signInWithEmailAndPassword(auth, email, password);
|
||||||
|
const token = await userCredential.user.getIdToken();
|
||||||
|
localStorage.setItem('TOKEN', token);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function register(email: string, password: string): Promise<string> {
|
||||||
|
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
|
||||||
|
const token = await userCredential.user.getIdToken();
|
||||||
|
localStorage.setItem('TOKEN', token);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function signOut(): Promise<void> {
|
||||||
|
await firebaseSignOut(auth);
|
||||||
|
localStorage.removeItem('TOKEN');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentUser(): User | null {
|
||||||
|
return auth.currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function signInWithGoogle(): Promise<string> {
|
||||||
|
const provider = new GoogleAuthProvider();
|
||||||
|
const userCredential = await signInWithPopup(auth, provider);
|
||||||
|
const token = await userCredential.user.getIdToken();
|
||||||
|
localStorage.setItem('TOKEN', token);
|
||||||
|
return token;
|
||||||
|
}
|
31
src/types/GameItem.ts
Normal file
31
src/types/GameItem.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
export interface GameItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: ItemType;
|
||||||
|
rarity: ItemRarity;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
effect: string;
|
||||||
|
equipTarget: ItemEquipTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ItemType {
|
||||||
|
Material = 'Material',
|
||||||
|
Consumable = 'Consumable',
|
||||||
|
Equipment = 'Equipment'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ItemRarity {
|
||||||
|
Common = 'Common', // White
|
||||||
|
Uncommon = 'Uncommon', // Green
|
||||||
|
Rare = 'Rare', // Blue
|
||||||
|
Legendary = 'Legendary' // Purple
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ItemEquipTarget {
|
||||||
|
None = 'None',
|
||||||
|
Head = 'Head',
|
||||||
|
Body = 'Body',
|
||||||
|
Legs = 'Legs',
|
||||||
|
Weapon = 'Weapon'
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user