This commit is contained in:
Arjun S
2025-08-04 08:56:13 +00:00
parent 5898f04950
commit 79c38efc94
4 changed files with 137 additions and 5 deletions

View File

@@ -74,7 +74,14 @@ interface UserJoinMessage extends AuthenticatedMessage {
type: 'user-join'; type: 'user-join';
} }
type Message = ClickMessage | PurchaseUpgradeMessage | ApplyMultiplierBonusMessage | UserJoinMessage | AdminBroadcastMessage; // Updated Message type interface EditUserMessage extends AuthenticatedMessage {
type: 'edit-user';
targetUserId: string;
clicks: number;
upgrades: Record<string, { owned: number; cost: number }>;
}
type Message = ClickMessage | PurchaseUpgradeMessage | ApplyMultiplierBonusMessage | UserJoinMessage | AdminBroadcastMessage | EditUserMessage; // Updated Message type
const UPGRADES: Upgrade[] = [ const UPGRADES: Upgrade[] = [
{ {
@@ -369,11 +376,29 @@ export default class GameServer implements Party.Server {
console.warn(`Unauthorized admin broadcast attempt from ${currentUserId}.`); console.warn(`Unauthorized admin broadcast attempt from ${currentUserId}.`);
} }
break; break;
case 'edit-user':
if (isAuthenticated && currentUserId === this.party.env.CLERK_ADMIN_USERID) {
this.handleEditUser(data);
} else {
console.warn(`Unauthorized edit user attempt from ${currentUserId}.`);
}
break;
} }
this.broadcast(); this.broadcast();
} }
handleEditUser(data: EditUserMessage) {
const { targetUserId, clicks, upgrades } = data;
const userState = this.gameState.users[targetUserId];
if (userState) {
userState.clicks = clicks;
userState.upgrades = upgrades;
this.updateGameMultipliers(targetUserId);
}
}
// handleUserJoin is now fully integrated into onMessage and can be removed or simplified. // handleUserJoin is now fully integrated into onMessage and can be removed or simplified.
// Removing it as its logic is now directly in onMessage. // Removing it as its logic is now directly in onMessage.
// handleUserJoin(data: UserJoinMessage) { // handleUserJoin(data: UserJoinMessage) {
@@ -475,7 +500,7 @@ export default class GameServer implements Party.Server {
} }
} }
updateGameMultipliers(userId: string) { updateGameMultipliers(userId: string, autoClickRateOverride?: number) {
const userState = this.gameState.users[userId]; const userState = this.gameState.users[userId];
if (!userState) return; if (!userState) return;
@@ -494,6 +519,10 @@ export default class GameServer implements Party.Server {
} }
}); });
if (autoClickRateOverride !== undefined) {
userState.autoClickRate = autoClickRateOverride;
}
this.setupAutoClicker(); this.setupAutoClicker();
} }

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useUser } from '@clerk/clerk-react'; import { useUser } from '@clerk/clerk-react';
import { usePartyKit } from '../hooks/usePartyKit'; import { usePartyKit } from '../hooks/usePartyKit';
import { UserState } from '../types';
interface AdminMessage { interface AdminMessage {
type: 'admin-message'; type: 'admin-message';
@@ -10,11 +11,15 @@ interface AdminMessage {
const AdminPage: React.FC = () => { const AdminPage: React.FC = () => {
const { user } = useUser(); const { user } = useUser();
const { sendMessage, lastMessage, getToken } = usePartyKit(); const { sendMessage, lastMessage, getToken, gameState, editUser } = usePartyKit();
const [broadcastMessage, setBroadcastMessage] = useState(''); const [broadcastMessage, setBroadcastMessage] = useState('');
const [targetUserId, setTargetUserId] = useState(''); const [targetUserId, setTargetUserId] = useState('');
const [receivedMessages, setReceivedMessages] = useState<AdminMessage[]>([]); const [receivedMessages, setReceivedMessages] = useState<AdminMessage[]>([]);
const [isAdmin, setIsAdmin] = useState(false); const [isAdmin, setIsAdmin] = useState(false);
const [selectedUser, setSelectedUser] = useState<UserState & { id: string } | null>(null);
const [clicks, setClicks] = useState(0);
const [autoClickRate, setAutoClickRate] = useState(0);
const [upgrades, setUpgrades] = useState<Record<string, { owned: number; cost: number }>>({});
// Replace with your actual admin user ID from .env or similar // Replace with your actual admin user ID from .env or similar
const CLERK_ADMIN_USERID = import.meta.env.VITE_CLERK_ADMIN_USERID; const CLERK_ADMIN_USERID = import.meta.env.VITE_CLERK_ADMIN_USERID;
@@ -62,6 +67,12 @@ const AdminPage: React.FC = () => {
} }
}; };
const handleEditUser = () => {
if (selectedUser) {
editUser(selectedUser.id, clicks, autoClickRate, upgrades);
}
};
if (!isAdmin) { if (!isAdmin) {
return ( return (
<div className="flex items-center justify-center min-h-screen bg-gray-900 text-white"> <div className="flex items-center justify-center min-h-screen bg-gray-900 text-white">
@@ -111,6 +122,80 @@ const AdminPage: React.FC = () => {
</div> </div>
<div className="p-6 bg-gray-800 rounded-lg shadow-lg"> <div className="p-6 bg-gray-800 rounded-lg shadow-lg">
<h2 className="text-2xl font-semibold mb-4">User Management</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<h3 className="text-xl font-semibold mb-4">Users</h3>
<div className="max-h-96 overflow-y-auto bg-gray-700 p-4 rounded-md">
<ul>
{gameState &&
Object.entries(gameState.users).map(([id, user]) => (
<li
key={id}
className={`cursor-pointer p-3 rounded-md mb-2 ${selectedUser?.id === id ? 'bg-blue-500' : 'bg-gray-600'}`}
onClick={() => {
setSelectedUser({ id, ...user });
setClicks(user.clicks);
setUpgrades(user.upgrades);
}}
>
{user.name}
</li>
))}
</ul>
</div>
</div>
{selectedUser && (
<div>
<h3 className="text-xl font-semibold mb-4">Edit User</h3>
<div className="mb-4">
<label htmlFor="clicks" className="block text-lg font-medium mb-2">
Clicks:
</label>
<input
type="number"
id="clicks"
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={clicks}
onChange={(e) => setClicks(Number(e.target.value))}
/>
</div>
<div className="mb-4">
<label htmlFor="autoClickRate" className="block text-lg font-medium mb-2">
Auto Click Rate:
</label>
<input
type="number"
id="autoClickRate"
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={autoClickRate}
onChange={(e) => setAutoClickRate(Number(e.target.value))}
/>
</div>
<div className="mb-4">
<label htmlFor="upgrades" className="block text-lg font-medium mb-2">
Upgrades (JSON):
</label>
<textarea
id="upgrades"
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={10}
value={JSON.stringify(upgrades, null, 2)}
onChange={(e) => setUpgrades(JSON.parse(e.target.value))}
></textarea>
</div>
<button
onClick={handleEditUser}
className="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-md transition duration-300 ease-in-out"
>
Save Changes
</button>
</div>
)}
</div>
</div>
<div className="p-6 bg-gray-800 rounded-lg shadow-lg mt-8">
<h2 className="text-2xl font-semibold mb-4">Received Admin Messages</h2> <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"> <div className="max-h-64 overflow-y-auto bg-gray-700 p-4 rounded-md">
{receivedMessages.length === 0 ? ( {receivedMessages.length === 0 ? (

View File

@@ -3,7 +3,7 @@ import PartySocket from 'partysocket';
import { GameState } from '../types'; import { GameState } from '../types';
import { useAuth, useUser } from '@clerk/clerk-react'; import { useAuth, useUser } from '@clerk/clerk-react';
const PARTY_HOST = import.meta.env.DEV ? 'localhost:1999' : 'bozo-clicker.arjunindia.partykit.dev'; const PARTY_HOST = import.meta.env.DEV ? 'https://solid-rotary-phone-pg69xw5vj7rc7r55-1999.app.github.dev' : 'bozo-clicker.arjunindia.partykit.dev';
export function usePartyKit() { export function usePartyKit() {
const { getToken, isLoaded, isSignedIn } = useAuth(); const { getToken, isLoaded, isSignedIn } = useAuth();
@@ -120,6 +120,22 @@ export function usePartyKit() {
const currentUserId = isSignedIn && user ? user.id : undefined; const currentUserId = isSignedIn && user ? user.id : undefined;
const currentUserName = isSignedIn && user ? (user.username || user.fullName || user.emailAddresses[0].emailAddress) : undefined; const currentUserName = isSignedIn && user ? (user.username || user.fullName || user.emailAddresses[0].emailAddress) : undefined;
const editUser = useCallback(async (targetUserId: string, clicks: number, autoClickRate: number, upgrades: any) => {
if (socket && isSignedIn && user) {
const token = await getToken();
socket.send(JSON.stringify({
type: 'edit-user',
token,
userId: user.id,
userName: user.username || user.fullName || user.emailAddresses[0].emailAddress,
targetUserId,
clicks,
autoClickRate,
upgrades
}));
}
}, [socket, isSignedIn, user, getToken]);
return { return {
gameState, gameState,
sendClick, sendClick,
@@ -129,6 +145,7 @@ export function usePartyKit() {
lastMessage, // Expose lastMessage lastMessage, // Expose lastMessage
userId: currentUserId, userId: currentUserId,
userName: currentUserName, userName: currentUserName,
getToken // Expose getToken getToken, // Expose getToken
editUser,
}; };
} }

View File

@@ -29,6 +29,7 @@ export interface Upgrade {
mascotTiers?: MascotTier[]; // New: for defining mascot tiers for friendBoost mascotTiers?: MascotTier[]; // New: for defining mascot tiers for friendBoost
oneTime?: boolean; // New: Indicates if the upgrade is a one-time purchase oneTime?: boolean; // New: Indicates if the upgrade is a one-time purchase
newsTitles?: string[]; // New: Array of news titles for the news upgrade newsTitles?: string[]; // New: Array of news titles for the news upgrade
youtubeId?: string; // New: YouTube ID for the upgrade
} }
export interface MascotTier { export interface MascotTier {