+
This commit is contained in:
Binary file not shown.
+68
-7
@@ -95,6 +95,21 @@ app.get('/games/:userId', (req, res) => {
|
||||
});
|
||||
|
||||
|
||||
// Fetch game details including game master
|
||||
app.get('/games/:gameId/master', (req, res) => {
|
||||
const gameId = req.params.gameId;
|
||||
db.get('SELECT * FROM games WHERE game_id = ?', [gameId], (err, row) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
if (!row) {
|
||||
return res.status(404).json({ error: 'Game not found' });
|
||||
}
|
||||
// Convert game_master_id to number for consistent comparison
|
||||
res.json(row);
|
||||
});
|
||||
});
|
||||
|
||||
// Join Game
|
||||
app.post('/joinGame/:gameId', (req, res) => {
|
||||
const gameId = req.params.gameId;
|
||||
@@ -162,18 +177,25 @@ app.post('/createGame', (req, res) => {
|
||||
// Fetch game Participant Characters
|
||||
app.get('/games/:gameId/playerchars', (req, res) => {
|
||||
const gameId = req.params.gameId;
|
||||
db.get('SELECT * FROM PlayerCharacter WHERE GameID = ?', [gameId], (err, row) => {
|
||||
db.all('SELECT * FROM PlayerCharacter WHERE GameID = ?', [gameId], (err, rows) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
res.json(row);
|
||||
// Process each row and convert BLOB to base64 if image exists
|
||||
const processedRows = rows.map(row => {
|
||||
if (row.Img) {
|
||||
row.Img = `data:image/jpeg;base64,${row.Img.toString('base64')}`;
|
||||
}
|
||||
return row;
|
||||
});
|
||||
})
|
||||
res.json(processedRows);
|
||||
});
|
||||
});
|
||||
|
||||
// Fetch game NPCs
|
||||
app.get('/games/:gameId/npcs', (req, res) => {
|
||||
const gameId = req.params.gameId;
|
||||
db.get('SELECT * FROM NPC WHERE GameID = ?', [gameId], (err, row) => {
|
||||
db.all('SELECT * FROM NPC WHERE GameID = ?', [gameId], (err, row) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
@@ -184,12 +206,19 @@ app.get('/games/:gameId/npcs', (req, res) => {
|
||||
// Fetch game Items
|
||||
app.get('/games/:gameId/items', (req, res) => {
|
||||
const gameId = req.params.gameId;
|
||||
db.get('SELECT * FROM Item WHERE GameID = ?', [gameId], (err, row) => {
|
||||
db.all(`
|
||||
SELECT Item.*, PlayerCharacter.CharName as OwnerName
|
||||
FROM Item
|
||||
LEFT JOIN PlayerCharacter ON Item.OwnerID = PlayerCharacter.CharID
|
||||
WHERE Item.GameID = ?`,
|
||||
[gameId],
|
||||
(err, rows) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
res.json(row);
|
||||
});
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
})
|
||||
|
||||
|
||||
@@ -324,6 +353,38 @@ app.get('/games/:gameId/:charId/items', (req, res) => {
|
||||
});
|
||||
|
||||
// Item Part
|
||||
// Create Item
|
||||
app.post('/games/item/create', upload.single('image'), (req, res) => {
|
||||
const {
|
||||
ItemName, Type, Art, Rarity, MaxDurability, CurrentDurability,
|
||||
GoldValue, Abilities, GameID, AP
|
||||
} = req.body;
|
||||
|
||||
const imageBuffer = req.file ? req.file.buffer : null;
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO Item (
|
||||
ItemName, Type, Art, Rarity, MaxDurability, CurrentDurability,
|
||||
GoldValue, Abilities, GameID, OwnerID, img, AP
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
|
||||
`);
|
||||
|
||||
stmt.run([
|
||||
ItemName, Type, Art, Rarity, MaxDurability, CurrentDurability,
|
||||
GoldValue, Abilities, GameID, imageBuffer, AP
|
||||
], function(err) {
|
||||
if (err) {
|
||||
console.error('Database error:', err);
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
res.status(201).json({
|
||||
message: 'Item created successfully!',
|
||||
itemId: this.lastID
|
||||
});
|
||||
});
|
||||
stmt.finalize();
|
||||
});
|
||||
|
||||
// Set Item Owner
|
||||
app.post('/games/:charId/:itemId/owner', (req, res) => {
|
||||
const gameId = req.params.gameId;
|
||||
|
||||
@@ -10,6 +10,8 @@ import JoinGame from './pages/joinGame';
|
||||
import StartGame from './pages/startGame';
|
||||
import Profile from './pages/profile';
|
||||
import Games from './pages/games.jsx';
|
||||
import GameMasterPage from './pages/gameMasterPage.jsx';
|
||||
import CreateItem from './pages/createItem.jsx';
|
||||
import CreateCharacter from './pages/createCharacter.jsx';
|
||||
import { UserProvider } from './context/UserContext.jsx';
|
||||
|
||||
@@ -61,6 +63,10 @@ function App() {
|
||||
path="/profile"
|
||||
element={<Profile isLoggedIn={isLoggedIn} />}
|
||||
/>
|
||||
<Route
|
||||
path='/games/:gameId/master'
|
||||
element={<GameMasterPage isLoggedIn={isLoggedIn} />}
|
||||
/>
|
||||
<Route
|
||||
path='/games/:gameId'
|
||||
element={<Games isLoggedIn={isLoggedIn} />}
|
||||
@@ -69,6 +75,11 @@ function App() {
|
||||
path='/create-Character'
|
||||
element={<CreateCharacter isLoggedIn={isLoggedIn} />}
|
||||
/>
|
||||
<Route
|
||||
path='/create-item'
|
||||
element={<CreateItem isLoggedIn={isLoggedIn} />}
|
||||
/>
|
||||
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
import React, { useState, useContext } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { UserContext } from '../context/UserContext';
|
||||
import axios from 'axios';
|
||||
import { Box, TextField, Button, Typography, Select, MenuItem, FormControl, InputLabel, Grid2, Card, CardContent, CardMedia } from '@mui/material';
|
||||
|
||||
import defaultItemImage from '../assets/default-item.png';
|
||||
|
||||
const CreateItem = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { userId } = useContext(UserContext);
|
||||
const gameId = new URLSearchParams(location.search).get('gameId');
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
ItemName: '', // Match DB column names exactly
|
||||
Type: '',
|
||||
Art: '',
|
||||
Rarity: 1,
|
||||
MaxDurability: 100,
|
||||
CurrentDurability: 100,
|
||||
GoldValue: 0,
|
||||
Abilities: '',
|
||||
AP: 0,
|
||||
GameID: gameId
|
||||
});
|
||||
|
||||
const [selectedImage, setSelectedImage] = useState(null);
|
||||
const [imagePreview, setImagePreview] = useState(null);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value
|
||||
});
|
||||
};
|
||||
|
||||
const handleImageChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
setSelectedImage(file);
|
||||
setImagePreview(URL.createObjectURL(file));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const formDataToSend = new FormData();
|
||||
|
||||
// Add all form fields with exact column names
|
||||
Object.keys(formData).forEach(key => {
|
||||
// Convert null values to empty strings to prevent SQL issues
|
||||
formDataToSend.append(key, formData[key] === null ? '' : formData[key]);
|
||||
});
|
||||
|
||||
// Add image if present
|
||||
if (selectedImage) {
|
||||
formDataToSend.append('image', selectedImage);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
'http://localhost:5000/games/item/create',
|
||||
formDataToSend,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 201) {
|
||||
navigate(`/games/${gameId}/master`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating item:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const inputStyles = {
|
||||
'& .MuiOutlinedInput-root': {
|
||||
'& fieldset': { borderColor: '#444' },
|
||||
'&:hover fieldset': { borderColor: '#764ACB' },
|
||||
'&.Mui-focused fieldset': { borderColor: '#764ACB' },
|
||||
},
|
||||
'& .MuiInputLabel-root': { color: '#fff' },
|
||||
'& .MuiInputBase-input': { color: '#fff' }
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3, background: 'rgba(30, 30, 47, 0.9)', borderRadius: '8px', marginTop: '40px' }}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Grid2 container spacing={3}>
|
||||
{/* Left Column - Image and Basic Info */}
|
||||
<Grid2 item xs={12} md={4}>
|
||||
<Box sx={{ border: '1px solid #444', borderRadius: '3px', p: 2, backgroundColor: '#2e2e3f', width: '330px' }}>
|
||||
<Card sx={{ backgroundColor: '#1e1e2f', color: '#fff' }}>
|
||||
{/* Image Upload Section */}
|
||||
{imagePreview ? (
|
||||
<Box component="label" htmlFor="image-upload" sx={{ cursor: 'pointer', position: 'relative' }}>
|
||||
<input
|
||||
accept="image/*"
|
||||
type="file"
|
||||
id="image-upload"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleImageChange}
|
||||
/>
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="300"
|
||||
image={imagePreview}
|
||||
alt="Item Preview"
|
||||
sx={{ borderRadius: '3px', objectFit: 'contain', margin: '0 auto', borderBottom: '1px solid #444' }}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
component="label"
|
||||
htmlFor="image-upload"
|
||||
sx={{
|
||||
height: 300,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#2e2e3f',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<input
|
||||
accept="image/*"
|
||||
type="file"
|
||||
id="image-upload"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleImageChange}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
component="span"
|
||||
sx={{
|
||||
backgroundColor: '#764ACB',
|
||||
'&:hover': { backgroundColor: '#9865f7' }
|
||||
}}
|
||||
>
|
||||
Upload Item Image
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Basic Item Info */}
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Item Name"
|
||||
name="ItemName" // Changed from itemName
|
||||
value={formData.ItemName}
|
||||
onChange={handleChange}
|
||||
required
|
||||
sx={{ ...inputStyles, mb: 2 }}
|
||||
/>
|
||||
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 item xs={6}>
|
||||
<FormControl fullWidth sx={{ mb: 2, width: '140px' }}> {/* Width for "Consumable" + padding */}
|
||||
<InputLabel sx={{ color: '#fff' }}>Type</InputLabel>
|
||||
<Select
|
||||
name="Type"
|
||||
value={formData.Type}
|
||||
onChange={handleChange}
|
||||
required
|
||||
sx={{ ...inputStyles }}
|
||||
>
|
||||
<MenuItem value="Weapon">Weapon</MenuItem>
|
||||
<MenuItem value="Armor">Armor</MenuItem>
|
||||
<MenuItem value="Consumable">Consumable</MenuItem>
|
||||
<MenuItem value="Quest">Quest Item</MenuItem>
|
||||
<MenuItem value="Other">Other</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid2>
|
||||
<Grid2 item xs={6}>
|
||||
<FormControl fullWidth sx={{ mb: 2, width: '120px' }}> {/* Width for "Potion" + padding */}
|
||||
<InputLabel sx={{ color: '#fff' }}>Art</InputLabel>
|
||||
<Select
|
||||
name="Art"
|
||||
value={formData.Art}
|
||||
onChange={handleChange}
|
||||
required
|
||||
sx={{ ...inputStyles }}
|
||||
>
|
||||
<MenuItem value="Sword">Sword</MenuItem>
|
||||
<MenuItem value="Axe">Axe</MenuItem>
|
||||
<MenuItem value="Bow">Bow</MenuItem>
|
||||
<MenuItem value="Shield">Shield</MenuItem>
|
||||
<MenuItem value="Staff">Staff</MenuItem>
|
||||
<MenuItem value="Potion">Potion</MenuItem>
|
||||
<MenuItem value="Other">Other</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Gold Value"
|
||||
name="GoldValue"
|
||||
type="number"
|
||||
value={formData.GoldValue}
|
||||
onChange={handleChange}
|
||||
required
|
||||
sx={{ ...inputStyles, mb: 2 }}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</Grid2>
|
||||
|
||||
{/* Right Column - Details */}
|
||||
<Grid2 item xs={12} md={8}>
|
||||
{/* Item Properties */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h5" sx={{ color: '#fff', mb: 2 }}>Item Properties</Typography>
|
||||
<Box sx={{ backgroundColor: '#2e2e3f', p: 2, borderRadius: '8px', border: '1px solid #444', width: '800px', justifyItems: 'center' }}>
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 item xs={12} md={4}>
|
||||
<FormControl fullWidth sx={{ mb: 2, width: '160px' }}> {/* Width for "Legendary" + padding */}
|
||||
<InputLabel sx={{ color: '#fff' }}>Rarity</InputLabel>
|
||||
<Select
|
||||
name="Rarity"
|
||||
value={formData.Rarity}
|
||||
onChange={handleChange}
|
||||
required
|
||||
sx={{ ...inputStyles }}
|
||||
>
|
||||
<MenuItem value={1}>Poor</MenuItem>
|
||||
<MenuItem value={2}>Common</MenuItem>
|
||||
<MenuItem value={3}>Uncommon</MenuItem>
|
||||
<MenuItem value={4}>Rare</MenuItem>
|
||||
<MenuItem value={5}>Epic</MenuItem>
|
||||
<MenuItem value={6}>Legendary</MenuItem>
|
||||
<MenuItem value={7}>Artifact</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid2>
|
||||
<Grid2 item xs={12} md={4}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Attack Power"
|
||||
name="AP"
|
||||
type="number"
|
||||
value={formData.AP}
|
||||
onChange={handleChange}
|
||||
sx={{ ...inputStyles }}
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 item xs={12} md={4}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Max Durability"
|
||||
name="MaxDurability"
|
||||
type="number"
|
||||
value={formData.MaxDurability}
|
||||
onChange={handleChange}
|
||||
required
|
||||
sx={{ ...inputStyles }}
|
||||
/>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Abilities */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h5" sx={{ color: '#fff', mb: 2 }}>Abilities</Typography>
|
||||
<Box sx={{ backgroundColor: '#2e2e3f', p: 2, borderRadius: '8px', border: '1px solid #444' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={2}
|
||||
name="Abilities"
|
||||
value={formData.Abilities}
|
||||
onChange={handleChange}
|
||||
sx={{ ...inputStyles }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
fullWidth
|
||||
sx={{
|
||||
backgroundColor: '#764ACB',
|
||||
'&:hover': { backgroundColor: '#9865f7' },
|
||||
mt: 3
|
||||
}}
|
||||
>
|
||||
Create Item
|
||||
</Button>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateItem;
|
||||
@@ -0,0 +1,267 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { UserContext } from '../context/UserContext';
|
||||
import axios from 'axios';
|
||||
import { Box, Typography, Grid2, Card, CardContent, CardMedia, Button, IconButton } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
|
||||
import defaultCharacterImage from '../assets/default-character.png';
|
||||
import defaultItemImage from '../assets/default-item.png';
|
||||
|
||||
const GameMasterPage = () => {
|
||||
const { userId } = useContext(UserContext);
|
||||
const { gameId } = useParams();
|
||||
const [playerCharacters, setPlayerCharacters] = useState([]);
|
||||
const [npcs, setNpcs] = useState([]);
|
||||
const [items, setItems] = useState([]);
|
||||
|
||||
// Fix useEffect data fetching
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Fetch player characters
|
||||
const pcsResponse = await axios.get(`http://localhost:5000/games/${gameId}/playerchars`);
|
||||
const processedPCs = Array.isArray(pcsResponse.data) ? pcsResponse.data : [pcsResponse.data];
|
||||
setPlayerCharacters(processedPCs.filter(pc => pc !== null));
|
||||
|
||||
// Fetch NPCs with different structure
|
||||
const npcsResponse = await axios.get(`http://localhost:5000/games/${gameId}/npcs`);
|
||||
const processedNPCs = Array.isArray(npcsResponse.data) ? npcsResponse.data : [npcsResponse.data];
|
||||
setNpcs(processedNPCs.filter(npc => npc !== null));
|
||||
|
||||
// Fetch Items
|
||||
const itemsResponse = await axios.get(`http://localhost:5000/games/${gameId}/items`);
|
||||
const processedItems = Array.isArray(itemsResponse.data) ? itemsResponse.data : [itemsResponse.data];
|
||||
setItems(processedItems.filter(item => item !== null));
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
const interval = setInterval(fetchData, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [gameId]);
|
||||
|
||||
const Section = ({ title, items, createPath, createText }) => (
|
||||
<Box sx={{
|
||||
mb: 4,
|
||||
backgroundColor: '#2e2e3f',
|
||||
p: 2,
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #444'
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h5" sx={{ color: '#fff' }}>{title}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
component={Link}
|
||||
to={`${createPath}?gameId=${gameId}`}
|
||||
startIcon={<AddIcon />}
|
||||
sx={{
|
||||
backgroundColor: '#764ACB',
|
||||
'&:hover': { backgroundColor: '#9865f7' }
|
||||
}}
|
||||
>
|
||||
{createText}
|
||||
</Button>
|
||||
</Box>
|
||||
<Grid2 container spacing={2}>
|
||||
{items.map((item, index) => (
|
||||
<Grid2 item xs={12} sm={6} md={3} key={index}>
|
||||
<Card sx={{
|
||||
backgroundColor: '#1e1e2f',
|
||||
color: '#fff',
|
||||
height: '95%', // Reduced height
|
||||
border: '1px solid #444',
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '8px'
|
||||
}}>
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="135" // Reduced from 140
|
||||
image={item.Img || defaultItemImage}
|
||||
alt={item.ItemName}
|
||||
className={`rarity-${item.Rarity} rarity-image`}
|
||||
sx={{
|
||||
objectFit: 'contain',
|
||||
width: '128px'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="div"
|
||||
className={`rarity-name-${item.Rarity}`}
|
||||
>
|
||||
{item.ItemName}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#bbb' }}>
|
||||
Type: {item.Type}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#bbb' }}>
|
||||
Value: {item.GoldValue}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#bbb' }}>
|
||||
Owner: {item.OwnerName || 'Unassigned'}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid2>
|
||||
))}
|
||||
</Grid2>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
// Update main container Box styling
|
||||
<Box sx={{
|
||||
p: 3,
|
||||
background: 'rgba(30, 30, 47, 0.9)',
|
||||
borderRadius: '8px',
|
||||
width: '1200px',
|
||||
margin: '80px auto 40px auto', // Increased top margin
|
||||
position: 'relative',
|
||||
minHeight: 'calc(100vh - 120px)' // Account for margins
|
||||
}}>
|
||||
<Typography variant="h4" sx={{ mb: 4, color: '#fff' }}>
|
||||
Game Master Dashboard
|
||||
</Typography>
|
||||
|
||||
{/* Player Characters Section */}
|
||||
<Box sx={{
|
||||
mb: 4,
|
||||
backgroundColor: '#2e2e3f',
|
||||
p: 2,
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #444'
|
||||
}}>
|
||||
<Typography variant="h5" sx={{ color: '#fff', mb: 2 }}>Player Characters</Typography>
|
||||
<Grid2 container spacing={2}>
|
||||
{playerCharacters.map((character, index) => (
|
||||
<Grid2 item xs={12} sm={6} md={3} key={index}>
|
||||
<Card sx={{
|
||||
backgroundColor: '#1e1e2f',
|
||||
color: '#fff',
|
||||
border: '1px solid #444',
|
||||
height: '100%'
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '8px'
|
||||
}}>
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="140"
|
||||
image={character.Img || defaultCharacterImage}
|
||||
alt={character.CharName}
|
||||
sx={{
|
||||
objectFit: 'contain',
|
||||
width: '140px',
|
||||
borderRadius: '3px'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<CardContent>
|
||||
<Typography variant="h6" component="div">
|
||||
{character.CharName}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#bbb' }}>
|
||||
{character.Race} - Level {character.Level}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#bbb' }}>
|
||||
{character.Job}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid2>
|
||||
))}
|
||||
</Grid2>
|
||||
</Box>
|
||||
|
||||
{/* NPCs Section */}
|
||||
<Box sx={{
|
||||
mb: 4,
|
||||
backgroundColor: '#2e2e3f',
|
||||
p: 2,
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #444'
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h5" sx={{ color: '#fff' }}>NPCs</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
component={Link}
|
||||
to={`/create-npc?gameId=${gameId}`}
|
||||
startIcon={<AddIcon />}
|
||||
sx={{
|
||||
backgroundColor: '#764ACB',
|
||||
'&:hover': { backgroundColor: '#9865f7' }
|
||||
}}
|
||||
>
|
||||
Create NPC
|
||||
</Button>
|
||||
</Box>
|
||||
<Grid2 container spacing={2}>
|
||||
{npcs.map((npc, index) => (
|
||||
<Grid2 item xs={12} sm={6} md={3} key={index}>
|
||||
<Card sx={{
|
||||
backgroundColor: '#1e1e2f',
|
||||
color: '#fff',
|
||||
border: '1px solid #444',
|
||||
height: '100%'
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '8px'
|
||||
}}>
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="140"
|
||||
image={npc.Img || defaultCharacterImage}
|
||||
alt={npc.CharName}
|
||||
sx={{
|
||||
objectFit: 'contain',
|
||||
width: '140px',
|
||||
borderRadius: '3px'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<CardContent>
|
||||
<Typography variant="h6" component="div">
|
||||
{npc.CharName}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#bbb' }}>
|
||||
{npc.Race}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#bbb' }}>
|
||||
{npc.Job}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid2>
|
||||
))}
|
||||
</Grid2>
|
||||
</Box>
|
||||
|
||||
{/* Items Section */}
|
||||
<Section
|
||||
title="Items"
|
||||
items={items}
|
||||
createPath="/create-item"
|
||||
createText="Create Item"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GameMasterPage;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useContext } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { Link, useParams, useNavigate } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { UserContext } from '../context/UserContext';
|
||||
import { Box, Typography, Grid2, Card, CardContent, CardMedia, Button, IconButton, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from '@mui/material';
|
||||
@@ -21,6 +21,7 @@ import './games.css';
|
||||
const GamesPage = () => {
|
||||
const { userId } = useContext(UserContext);
|
||||
const { gameId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [character, setCharacter] = useState(null);
|
||||
const [inventory, setInventory] = useState([]);
|
||||
const [isEditOpen, setIsEditOpen] = useState(false);
|
||||
@@ -68,6 +69,29 @@ const GamesPage = () => {
|
||||
return () => clearInterval(interval);
|
||||
}, [character]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkGameMaster = async () => {
|
||||
if (!userId || !gameId) return;
|
||||
|
||||
try {
|
||||
console.log('Checking if user is game master...');
|
||||
const response = await axios.get(`http://localhost:5000/games/${gameId}/master`);
|
||||
console.log('Game data:', response.data);
|
||||
console.log('User ID:', userId);
|
||||
console.log('Game master ID:', response.data.game_master_id);
|
||||
|
||||
if (parseInt(userId) === parseInt(response.data.game_master_id)) {
|
||||
console.log('User is game master, redirecting...');
|
||||
navigate(`/games/${gameId}/master`, { replace: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking game master:', error);
|
||||
}
|
||||
};
|
||||
|
||||
checkGameMaster();
|
||||
}, [userId, gameId, navigate]);
|
||||
|
||||
const handleEditOpen = () => {
|
||||
setNewDescription(character.description);
|
||||
setIsEditOpen(true);
|
||||
|
||||
Reference in New Issue
Block a user