adminpage

This commit is contained in:
2025-08-03 23:18:49 +05:30
parent f618a02ce3
commit 511bbb87b4
6 changed files with 279 additions and 9 deletions

View File

@@ -12,14 +12,22 @@ import { useAuth } from '@clerk/clerk-react';
import { ClickableMascot } from './components/ClickableMascot'; // Import ClickableMascot component
import { ClickableMascot as ClickableMascotType, Upgrade } from './types'; // Import ClickableMascotType and Upgrade
import { UPGRADES } from './config/upgrades'; // Import UPGRADES for upgrade config
import { useLocation, Link } from 'wouter'; // Import wouter hooks
import AdminPage from './components/AdminPage'; // Import AdminPage
function App() {
const { isSignedIn, isLoaded } = useAuth();
const { gameState, sendClick, purchaseUpgrade, userId, sendMascotClickBonus } = usePartyKit(); // Renamed sendShoominionClickBonus
const { isSignedIn, isLoaded, userId: clerkUserId } = useAuth(); // Get clerkUserId from useAuth
const { gameState, sendClick, purchaseUpgrade, userId, sendMascotClickBonus, lastMessage } = usePartyKit(); // Renamed sendShoominionClickBonus
const [celebrationMessage, setCelebrationMessage] = useState<string | null>(null);
const [previousMilestones, setPreviousMilestones] = useState<Record<string, boolean>>({});
const [mascotEntities, setMascotEntities] = useState<ClickableMascotType[]>([]);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const [location] = useLocation(); // Get current location from wouter
const [adminBroadcastMessage, setAdminBroadcastMessage] = useState<string | null>(null); // New state for admin messages
// Admin user ID from .env
const CLERK_ADMIN_USERID = import.meta.env.VITE_CLERK_ADMIN_USERID;
const isAdmin = clerkUserId === CLERK_ADMIN_USERID;
// Effect for milestone celebrations
useEffect(() => {
@@ -42,6 +50,21 @@ function App() {
}
}, [gameState, previousMilestones]);
// Effect for receiving admin broadcast messages
useEffect(() => {
if (lastMessage) {
try {
const parsedMessage = JSON.parse(lastMessage);
if (parsedMessage.type === 'admin-message') {
setAdminBroadcastMessage(parsedMessage.message);
setTimeout(() => setAdminBroadcastMessage(null), 7000); // Message disappears after 7 seconds
}
} catch (error) {
console.error('Failed to parse last message in App.tsx:', error);
}
}
}, [lastMessage]);
// Effect for spawning mascots
useEffect(() => {
if (!gameState) return;
@@ -152,12 +175,24 @@ function App() {
const userBonusMultiplier = isSignedIn && userId ? gameState.users[userId]?.bonusMultiplier || 1 : 1;
const effectiveClickMultiplier = gameState.clickMultiplier * userBonusMultiplier;
// Render the AdminPage if the current location is /admin
if (location === '/admin') {
return <AdminPage />;
}
return (
<div className="min-h-screen relative overflow-x-hidden">
<Background background={gameState.currentBackground} />
{/* User Button */}
<div className="absolute top-4 right-4 z-50">
{/* User Button and Admin Link */}
<div className="absolute top-4 right-4 z-50 flex items-center space-x-4">
{isAdmin && (
<Link href="/admin">
<a className="text-white bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-md font-bold transition duration-300 ease-in-out">
Admin Page
</a>
</Link>
)}
<SignedIn>
<UserButton />
</SignedIn>
@@ -172,6 +207,15 @@ function App() {
</div>
)}
{/* Admin Broadcast Message */}
{adminBroadcastMessage && (
<div className="fixed top-20 left-0 right-0 z-50 flex justify-center pt-8">
<div className="bg-red-600 text-white px-8 py-4 rounded-full text-2xl font-bold shadow-2xl animate-pulse border-4 border-white">
📢 Admin Message: {adminBroadcastMessage} 📢
</div>
</div>
)}
<div className="container mx-auto px-4 py-8 relative z-10">
{/* Header */}
<div className="mb-8">

View File

@@ -0,0 +1,133 @@
import React, { useState, useEffect } from 'react';
import { useUser } from '@clerk/clerk-react';
import { usePartyKit } from '../hooks/usePartyKit';
interface AdminMessage {
type: 'admin-message';
message: string;
sender: string;
}
const AdminPage: React.FC = () => {
const { user } = useUser();
const { sendMessage, lastMessage, getToken } = usePartyKit();
const [broadcastMessage, setBroadcastMessage] = useState('');
const [targetUserId, setTargetUserId] = useState('');
const [receivedMessages, setReceivedMessages] = useState<AdminMessage[]>([]);
const [isAdmin, setIsAdmin] = useState(false);
// Replace with your actual admin user ID from .env or similar
const CLERK_ADMIN_USERID = import.meta.env.VITE_CLERK_ADMIN_USERID;
useEffect(() => {
if (user?.id === CLERK_ADMIN_USERID) {
setIsAdmin(true);
} else {
setIsAdmin(false);
}
}, [user, CLERK_ADMIN_USERID]);
useEffect(() => {
if (lastMessage) {
try {
const parsedMessage = JSON.parse(lastMessage);
if (parsedMessage.type === 'admin-message') {
setReceivedMessages((prev) => [...prev, parsedMessage]);
}
} catch (error) {
console.error('Failed to parse last message:', error);
}
}
}, [lastMessage]);
const handleBroadcast = async () => {
if (!user || !isAdmin) {
alert('You are not authorized to send admin messages.');
return;
}
if (broadcastMessage.trim()) {
const token = await getToken(); // Get the token using the exposed getToken from usePartyKit
const messagePayload = {
type: 'admin-broadcast',
token: token, // Send the Clerk token
userId: user.id,
userName: user.fullName || user.id,
message: broadcastMessage.trim(),
...(targetUserId.trim() && { targetUserId: targetUserId.trim() }),
};
sendMessage(JSON.stringify(messagePayload));
setBroadcastMessage('');
setTargetUserId('');
}
};
if (!isAdmin) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-900 text-white">
<p className="text-xl">Access Denied: You are not an administrator.</p>
</div>
);
}
return (
<div className="min-h-screen bg-gray-900 text-white p-8">
<h1 className="text-3xl font-bold mb-6">Admin Broadcast Page</h1>
<div className="mb-8 p-6 bg-gray-800 rounded-lg shadow-lg">
<h2 className="text-2xl font-semibold mb-4">Send Broadcast Message</h2>
<div className="mb-4">
<label htmlFor="message" className="block text-lg font-medium mb-2">
Message:
</label>
<textarea
id="message"
className="w-full p-3 rounded-md bg-gray-700 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={4}
value={broadcastMessage}
onChange={(e) => setBroadcastMessage(e.target.value)}
placeholder="Enter your broadcast message here..."
></textarea>
</div>
<div className="mb-4">
<label htmlFor="targetUserId" className="block text-lg font-medium mb-2">
Target User ID (Optional, leave blank for all users):
</label>
<input
type="text"
id="targetUserId"
className="w-full p-3 rounded-md bg-gray-700 border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={targetUserId}
onChange={(e) => setTargetUserId(e.target.value)}
placeholder="e.g., user_123abc"
/>
</div>
<button
onClick={handleBroadcast}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-md transition duration-300 ease-in-out"
>
Send Broadcast
</button>
</div>
<div className="p-6 bg-gray-800 rounded-lg shadow-lg">
<h2 className="text-2xl font-semibold mb-4">Received Admin Messages</h2>
<div className="max-h-64 overflow-y-auto bg-gray-700 p-4 rounded-md">
{receivedMessages.length === 0 ? (
<p className="text-gray-400">No admin messages received yet.</p>
) : (
<ul>
{receivedMessages.map((msg, index) => (
<li key={index} className="mb-2 p-3 bg-gray-600 rounded-md">
<strong>{msg.sender}:</strong> {msg.message}
</li>
))}
</ul>
)}
</div>
</div>
</div>
);
};
export default AdminPage;

View File

@@ -10,6 +10,7 @@ export function usePartyKit() {
const { user } = useUser();
const [gameState, setGameState] = useState<GameState | null>(null);
const [socket, setSocket] = useState<PartySocket | null>(null);
const [lastMessage, setLastMessage] = useState<string | null>(null); // New state for last message
// Generate a persistent guest ID if the user is not signed in
const [guestId] = useState(() => {
let storedGuestId = localStorage.getItem('bozo_guest_id');
@@ -59,6 +60,7 @@ export function usePartyKit() {
if (data.type === 'game-state') {
setGameState(data.state);
}
setLastMessage(event.data); // Store the raw last message
};
setSocket(ws);
@@ -105,6 +107,14 @@ export function usePartyKit() {
}
}, [socket, isSignedIn, user, getToken]);
// Determine the userId and userName to expose based on sign-in status
// Only expose Clerk user ID/name if signed in, otherwise undefined
const sendMessage = useCallback((message: string) => {
if (socket) {
socket.send(message);
}
}, [socket]);
// Determine the userId and userName to expose based on sign-in status
// Only expose Clerk user ID/name if signed in, otherwise undefined
const currentUserId = isSignedIn && user ? user.id : undefined;
@@ -115,7 +125,10 @@ export function usePartyKit() {
sendClick,
purchaseUpgrade,
sendMascotClickBonus,
sendMessage, // Expose sendMessage
lastMessage, // Expose lastMessage
userId: currentUserId,
userName: currentUserName
userName: currentUserName,
getToken // Expose getToken
};
}