Active player list
This commit is contained in:
55
islands/players.tsx
Normal file
55
islands/players.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { StatusManager } from "./statusManager.tsx";
|
||||
import { IS_BROWSER } from "$fresh/runtime.ts";
|
||||
import { PlayerData } from "../util/players.ts";
|
||||
import { Button } from "../components/Button.tsx";
|
||||
|
||||
export function ActivePlayerList() {
|
||||
const [players, setPlayers] = useState<PlayerData[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!IS_BROWSER) return;
|
||||
configureEventSource();
|
||||
}, []);
|
||||
|
||||
const eventSource = useRef<EventSource>();
|
||||
|
||||
const configureEventSource = () => {
|
||||
if (eventSource.current?.OPEN) eventSource.current.close();
|
||||
eventSource.current = new EventSource("/api/players");
|
||||
eventSource.current.addEventListener("players", (e) => {
|
||||
setPlayers(JSON.parse(e.data));
|
||||
});
|
||||
};
|
||||
|
||||
const followStatusManager = (action: string) => {
|
||||
if (action === "started" || action === "restarted") {
|
||||
configureEventSource();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StatusManager onAction={followStatusManager} />
|
||||
<div class="grid grid-cols-2 p-8">
|
||||
<h2 class="col-span-2 font-pixel text-xl">Active Players</h2>
|
||||
{players.map((p) => <PlayerCard player={p} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PlayerCard({ player }: { player: PlayerData }) {
|
||||
return (
|
||||
<div class="flex gap-4">
|
||||
<img class="w-16" src={player.avatar} alt={`${player.username}'s avatar`} />
|
||||
<div>
|
||||
<h3>{player.username}</h3>
|
||||
<small class="opacity-50">UUID: {player.id}</small>
|
||||
<div>
|
||||
Did you know that bats are the only mammal that can fly?
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
51
islands/statusManager.tsx
Normal file
51
islands/statusManager.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { JSX } from "preact";
|
||||
import { Button } from "../components/Button.tsx";
|
||||
import { ManageAction } from "../routes/api/manage.ts";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { IS_BROWSER } from "$fresh/runtime.ts";
|
||||
|
||||
export function StatusManager(
|
||||
props: JSX.HTMLAttributes<HTMLDivElement> & {
|
||||
onAction?: (res: string) => void;
|
||||
},
|
||||
) {
|
||||
const sendCommand = async (action: ManageAction) => {
|
||||
console.log(action);
|
||||
const res = await fetch("/api/manage", {
|
||||
method: "POST",
|
||||
body: action,
|
||||
});
|
||||
const body = await res.text();
|
||||
props.onAction && props.onAction(body);
|
||||
};
|
||||
|
||||
const [status, setStatus] = useState('');
|
||||
|
||||
const getStatus = async () => {
|
||||
const res = await fetch('/api/manage');
|
||||
const body = await res.text();
|
||||
|
||||
setStatus(body);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (IS_BROWSER) getStatus();
|
||||
},[])
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
{!!status && <small>Server is {status}</small>}
|
||||
<div class="flex gap-4">
|
||||
<Button color="wasabi" disabled={!status || status === 'running'} onClick={() => sendCommand(ManageAction.start)}>
|
||||
Start
|
||||
</Button>
|
||||
<Button color="fire" disabled={!status || status === 'stopped'} onClick={() => sendCommand(ManageAction.stop)}>
|
||||
Stop
|
||||
</Button>
|
||||
<Button color="sky" disabled={!status} onClick={() => sendCommand(ManageAction.restart)}>
|
||||
Restart
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,19 +1,40 @@
|
||||
import { IS_BROWSER } from "$fresh/runtime.ts";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { JSX } from "preact/jsx-runtime";
|
||||
import { Sockpuppet } from "puppet/client";
|
||||
|
||||
|
||||
export function Terminal(props: { channelId: string }) {
|
||||
const puppet = useRef(new Sockpuppet("ws://sockpuppet.cyborggrizzly.com"));
|
||||
const [lines, setLines] = useState<string[]>([]);
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
const storeKey = 'commandHistory';
|
||||
const [commandHistory, setCommandHistory] = useState<string[]>(JSON.parse(localStorage.getItem(storeKey) || '[]'));
|
||||
const [historyIndex, setHistoryIndex] = useState(commandHistory.length);
|
||||
|
||||
const [command, setCommand] = useState("");
|
||||
|
||||
const changeHistoryIndex = (by: number) => setHistoryIndex(i => i + by);
|
||||
|
||||
useEffect(() => {
|
||||
if (!IS_BROWSER) return;
|
||||
puppet.current.joinChannel(props.channelId, (line) => {
|
||||
setLines((l) => [...l, line]);
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
console.log(e.key)
|
||||
switch (e.key) {
|
||||
case 'Up':
|
||||
case 'ArrowUp':
|
||||
changeHistoryIndex(-1);
|
||||
break;
|
||||
case 'Down':
|
||||
case 'ArrowDown':
|
||||
changeHistoryIndex(1);
|
||||
break;
|
||||
}
|
||||
})
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -24,22 +45,38 @@ export function Terminal(props: { channelId: string }) {
|
||||
|
||||
const sendCommand = (e: Event) => {
|
||||
e.preventDefault();
|
||||
puppet.current.getChannel(props.channelId)?.send(command);
|
||||
puppet.current.getChannel(props.channelId)?.send(historyIndex === commandHistory.length ? command : commandHistory[historyIndex]);
|
||||
setCommandHistory(c => [...c, command]);
|
||||
setHistoryIndex(commandHistory.length + 1)
|
||||
setCommand("");
|
||||
};
|
||||
|
||||
const handleCommandUpdate = (e: JSX.TargetedEvent<HTMLInputElement, Event>) => {
|
||||
const value = e.currentTarget.value;
|
||||
|
||||
if (historyIndex !== commandHistory.length) {
|
||||
setHistoryIndex(commandHistory.length);
|
||||
}
|
||||
|
||||
setCommand(value || '')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(storeKey, JSON.stringify(commandHistory.slice(0,100)));
|
||||
}, [commandHistory])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div ref={divRef} class="flex flex-col h-[600px] w-full overflow-auto">
|
||||
{lines.map((l) => <div class="font-mono w-full">{l}</div>)}
|
||||
{lines.map((l) => <pre class="font-mono w-full">{l}</pre>)}
|
||||
</div>
|
||||
<div>
|
||||
<div class="mt-4">
|
||||
<form onSubmit={sendCommand}>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full bg-smoke-600 text-white"
|
||||
value={command}
|
||||
onInput={(e) => setCommand((e.target as HTMLInputElement).value)}
|
||||
value={commandHistory[historyIndex] || command}
|
||||
onInput={handleCommandUpdate}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user