initial commit

This commit is contained in:
Soph :3 2024-05-22 11:35:43 +03:00
commit 6f5a39c212
Signed by: sophie
GPG key ID: EDA5D222A0C270F2
47 changed files with 3601 additions and 0 deletions

132
web/src/components/App.tsx Normal file
View file

@ -0,0 +1,132 @@
import Chat from "./Chat";
import ChatBar from "./ChatBar";
import Sidebar from "./Sidebar";
import useWebSocket from 'react-use-websocket';
import { useEffect, useState } from "preact/hooks";
import toast, { Toaster } from 'react-hot-toast';
import People from "./People";
import { apiDomain } from "../treaty";
export default function App() {
// #region State handling
const [socketUrl, setSocketUrl] = useState(apiDomain.replace("http", "ws") + '/api/ws/');
const [roomName, setRoomName] = useState(localStorage.lastRoom);
const [sendRoomHidden, setSendRoomHidden] = useState<boolean>(localStorage.lastHidden == "true");
const [accountState, setAccountState] = useState<undefined | {
username: string;
}>(undefined)
const [staff, setStaff] = useState<boolean>(false);
const [rooms, setRooms] = useState([]);
const [messageHistory, setMessageHistory] = useState<{
userID: string;
content: string;
images?: string[];
time: number;
username: string;
}[]>([]);
const [people, setPeople] = useState([]);
const [user, setUser] = useState<any>({});
const [currentPeopleView, setCurrentPeopleView] = useState("people");
// #endregion State handling
const { sendMessage, lastMessage, readyState } = useWebSocket(socketUrl, {
shouldReconnect: (closeEvent) => {
if (accountState == undefined) return false;
return true;
},
});
useEffect(() => {
if (sendRoomHidden) localStorage.lastHidden = "true";
else localStorage.lastHidden = "false";
sendMessage(JSON.stringify({
type: "room",
room: roomName,
hidden: sendRoomHidden
}))
}, [roomName, sendRoomHidden])
useEffect(() => {
if (lastMessage !== null) {
let data;
try {
data = JSON.parse(lastMessage.data);
} catch {
return;
}
if (data.type == "hello") {
toast.success("Connected to WS!", {
position: "bottom-right"
})
setStaff(data.staff)
sendMessage(JSON.stringify({
type: "room",
room: roomName
}))
}
if (data.type == "notification") {
toast(data.message);
}
if (data.type == "history") {
setMessageHistory(data.messages);
}
if (data.type == "message") {
setMessageHistory(z => z.concat([data.message]));
}
if (data.type == "rooms") {
setRooms(data.rooms);
}
if (data.type == "people") {
setPeople(data.people);
}
if (data.type == "room") {
setRoomName(data.room);
}
if(data.type == "updateUser") {
//{type: 'updateUser', updateType: 'pfp', id: 'ieuavxi5nqbqpgow'}
console.log(data);
setPeople(z => {
return z.map(g => {
if(g.id == data.id) {
g = {...g,r:Math.random()}
if(data.updateType == "status") {
g = {...g,r:Math.random(),status:data.data}
}
}
return g;
});
})
}
}
}, [lastMessage]);
return (
<>
<div class="flex flex-row w-[100vw] h-[100vh] bg-slate-200 ">
<div class="flex-grow max-w-60 flex flex-col bg-slate-300 rounded-tr-xl h-full">
<Sidebar setCurrentPeopleView={setCurrentPeopleView} setUser={setUser} setSocketUrl={setSocketUrl} staff={staff} accountState={accountState} setAccountState={setAccountState} setRoomName={setRoomName} roomName={roomName} rooms={rooms} setSendRoomHidden={setSendRoomHidden}></Sidebar>
</div>
<div class="flex-grow flex flex-col">
<div class="h-full overflow-y-scroll">
<Chat messageHistory={messageHistory}></Chat>
</div>
<div class="w-full bg-slate-300 h-15">
<ChatBar sendMessage={sendMessage} readyState={readyState}></ChatBar>
</div>
</div>
<People currentPeopleView={currentPeopleView} setCurrentPeopleView={setCurrentPeopleView} user={user} setUser={setUser} people={people} roomName={roomName} staff={staff} messageHistory={messageHistory}></People>
</div>
<Toaster />
</>
)
}

View file

@ -0,0 +1,79 @@
import { useEffect, useRef, useState } from "preact/hooks";
import { apiDomain } from "../treaty";
import Person from "../miniComponents/Person";
function truncateText(text, length) {
let text2 = text;
if (length < text.length) {
text2 = text2.slice(0, length);
text2 += "..."
}
return text2;
}
export default function Chat({ messageHistory }) {
const chatRef = useRef<HTMLDivElement>();
function SpotifyWidget({ id, type }: { id: string, type: "episode" | "track" }) {
return <iframe style="border-radius:12px" src={"https://open.spotify.com/embed/" + type + "/" + id + "?utm_source=generator"} width="100%" height="152" frameBorder="0" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>
}
function ChatMessage({ username, id, chatMessage, time, images }) {
const [isClosed, setIsClosed] = useState<boolean>(true);
function onClick() {
setIsClosed(z => !z);
}
const tenorRegex = /(https:\/\/media\.tenor\.com\/[^\/]*\/[^.]*\.gif)/gm;
return <div>
<Person user={{username,id}} time={time}></Person>
<div class="m-2 text-black " onClick={onClick}>
{isClosed ? truncateText(chatMessage.replace(tenorRegex, ""), 200) : chatMessage}
</div>
<div>
<div class="m-2">
{(() => {
const reg = /.*https:\/\/open.spotify.com\/(track|episode)\/([0-9a-zA-Z]*).*/gm;
if(reg.test(chatMessage)) {
const type = chatMessage.replace(reg, "$1");
const id = chatMessage.replace(reg, "$2");
return <SpotifyWidget type={type} id={id}></SpotifyWidget>
}
})()}
</div>
</div>
<div class="m-2">
{images.map(z => {
return <img src={apiDomain+"/images/" + z} class="max-w-72"></img>
})}
{
[...chatMessage.matchAll(tenorRegex)].map(z => {
return <img src={z}></img>
})
}
</div>
</div>
}
useEffect(() => {
setTimeout(() => { // TODO: This is a ugly workaround, please fix ASAP!
if(chatRef.current.lastElementChild) {
chatRef.current.lastElementChild.scrollIntoView();
}
}, 200)
}, [messageHistory]);
return (
<>
<div class="" ref={chatRef}>
{
messageHistory.map(z => {
return <ChatMessage id={z.userID} images={z.images || []} time={z.time} username={z.username} chatMessage={z.content} ></ChatMessage>;
})
}
</div>
</>
)
}

View file

@ -0,0 +1,81 @@
import toast from "react-hot-toast";
import { ReadyState } from "react-use-websocket";
import { trty } from "../treaty";
import { useRef, useState } from "preact/hooks";
import Input from "../miniComponents/Input";
import GifPicker from "gif-picker-react";
export default function ChatBar({ sendMessage, readyState, }) {
const [images, setImages] = useState<string[]>([]);
const [qpickerOpen, setGpickerOpen] = useState<boolean>(false);
const chatInputRef = useRef<HTMLInputElement>();
async function uploadImage() {
let image_input = document.getElementById("bar_input") as HTMLInputElement
if (!image_input) {
image_input = document.createElement("input");
image_input.style.display = "none";
image_input.type = "file"
image_input.accept = "image/jpeg, image/png, image/jpg"
image_input.id = "bar_input"
document.body.appendChild(image_input)
image_input.addEventListener("change", () => {
toast.promise((async () => {
const req = await trty.api.profile.uploadImage.post({
file: image_input.files[0]
})
if (!(req.data instanceof Response)) {
const uploadId = req.data.uploadId;
setImages(z => z.concat([uploadId]))
}
})(), {
loading: "Uploading..",
success: "Uploaded!",
error: "Upload failed. :("
})
})
}
image_input.click()
}
return (
//class="h-6 bg-slate-500 text-white rounded-md p-4 w-full"
<>
<div class="m-2 flex flex-row items-center">
<Input extraClass="w-full h-full p-3" ref={chatInputRef} readOnly={readyState !== ReadyState.OPEN} placeholder={readyState == ReadyState.OPEN ? "write something here" : "not connected yet.."} onKeyUp={(e) => {
const me = e.target as HTMLInputElement;
if (e.code == "Enter" && me.value.trim()) {
sendMessage(JSON.stringify({
type: "message",
message: me.value.trim(),
images
}));
setImages([]);
me.value = "";
}
}}></Input>
<button onClick={uploadImage}>
<i class="fa-solid fa-upload text-3xl m-2 text-black "></i>
</button>
<button onClick={() => {
setGpickerOpen(z => !z);
}}>
<i class="fa-solid fa-explosion text-3xl m-2 text-black "></i>
</button>
<div class="absolute bottom-14 right-0">
{ qpickerOpen? <GifPicker onGifClick={(img) => {
console.log(img);
chatInputRef.current.value += img.url;
setGpickerOpen(false);
}} tenorApiKey="AIzaSyCDq45efUHa0ZBa9RV7gl3v8WqxyQlb4X0" clientKey={"trillium"}></GifPicker> : ""}
</div>
</div>
</>
)
}

View file

@ -0,0 +1,76 @@
import { useRef } from "preact/hooks";
import PeopleView from "./peopleViews/PeopleView";
import PersonView from "./peopleViews/PersonView";
import Button from "../miniComponents/Button";
export default function ({
roomName,
people,
staff,
messageHistory,
user,
setUser,
currentPeopleView,
setCurrentPeopleView
}) {
const peopleBarRef = useRef<HTMLDivElement>();
const peopleViews = {
people: <PeopleView setUser={setUser} staff={staff} setCurrentPeopleView={setCurrentPeopleView} roomName={roomName} people={people}></PeopleView>,
person: <PersonView user={user}></PersonView>
}
return <>
<div id="peopleBar" class="bg-slate-300 w-52 overflow-auto flex flex-col break-all" style="display:none;" ref={peopleBarRef}>
{
(() => {
const sidebar = peopleViews[currentPeopleView];
return sidebar;
})()
}
{
(() => {
const e = <Button extraClass="mt-auto" onClick={() => {
console.log(messageHistory)
const blob = new Blob([JSON.stringify(messageHistory)], {type: "application/json"})
const elem = window.document.createElement('a');
elem.href = window.URL.createObjectURL(blob);
elem.download = `${new Date().toUTCString()} trillium logs.json`;
document.body.appendChild(elem);
elem.click();
document.body.removeChild(elem);
}}>Export logs</Button>
if (!staff) return e;
if (currentPeopleView == "person") {
return <Button extraClass="mt-auto" onClick={
() => {
setCurrentPeopleView("people");
}
} >
View people
</Button>
} else if (currentPeopleView == "people") {
return e;
}
})()
}
</div>
<button class="absolute top-4 right-6" id="peopleBarButton" onClick={
(e) => {
const tt = (e.target as HTMLDivElement).parentElement;
if (peopleBarRef.current.style.display != "none") {
tt.className = tt.className.replace("right-64", 'right-6')
peopleBarRef.current.style.display = "none"
} else {
tt.className = tt.className.replace('right-6', "right-64")
peopleBarRef.current.style.display = "flex";
}
}
}>
<i class="fa-solid fa-ellipsis text-5xl text-black "></i>
</button></>;
}

View file

@ -0,0 +1,55 @@
import { useEffect, useState } from "preact/hooks"
import SidebarAccount from "./sidebarViews/SidebarAccount"
import SidebarRooms from "./sidebarViews/SidebarRooms";
import SidebarStaff from "./sidebarViews/SidebarStaff";
export default function Sidebar({ setCurrentPeopleView, setUser, setSocketUrl, staff, setRoomName, roomName, rooms, setSendRoomHidden, accountState, setAccountState }) {
const [currentSidebarView, setCurrentSidebarView] = useState<keyof typeof sidebarViews>(localStorage.sidebarView || "account");
const sidebarViews = {
account: <SidebarAccount setSocketUrl={setSocketUrl} accountState={accountState} setAccountState={setAccountState} currentView={currentSidebarView}></SidebarAccount>,
rooms: <SidebarRooms currentView={currentSidebarView} setSendRoomHidden={setSendRoomHidden} setRoomName={setRoomName} roomName={roomName} rooms={rooms}></SidebarRooms>,
staff: <SidebarStaff setCurrentPeopleView={setCurrentPeopleView} setUser={setUser} currentView={currentSidebarView}></SidebarStaff>
}
useEffect(() => {
localStorage.sidebarView = currentSidebarView;
}, [currentSidebarView])
return (
<>
<div class="self-top self-center my-5 w-[70%]">
<img src="logo.svg"></img>
<h1 class='text-center text-5xl font-bold'>trillium</h1>
</div>
<div class=" text-white h-full overflow-y-auto">
{
(() => {
const sidebar = sidebarViews[currentSidebarView];
return sidebar;
})()
}
</div>
<div class="mt-auto">
<div class="p-2 rounded-lg m-2 bg-slate-500 text-white text-center">
<button class={currentSidebarView == "account" ? "text-green-300" : ""} onClick={() => {
setCurrentSidebarView("account")
}}>
<i class="fa-solid fa-user text-4xl mr-2 hover:underline"></i>
</button>
<button class={currentSidebarView == "rooms" ? "text-green-300" : ""} onClick={() => {
setCurrentSidebarView("rooms")
}}>
<i class="fa-solid fa-users-rays text-4xl mr-2 hover:underline"></i>
</button>
{staff ?
<button class={currentSidebarView == "staff" ? "text-green-300" : ""} onClick={() => {
setCurrentSidebarView("staff")
}}>
<i class="fa-solid fa-user-shield text-4xl mr-2 hover:underline"></i>
</button> : ""}
</div>
</div>
</>
)
}

View file

@ -0,0 +1,21 @@
import toast from "react-hot-toast";
import Person from "../../miniComponents/Person";
import { useEffect } from "preact/hooks";
export default function ({ people, staff, roomName, setCurrentPeopleView, setUser }) {
return <>
<h1 class="text-center text-black"><kbd>{roomName}</kbd></h1>
<h1 class="text-center text-black">{people.length} pe{people.length == 1 ? "rson" : "ople"}</h1>
{people.map(z => {
return <Person user={z} viewStatus={true} onClick={async () => {
if (staff) {
setUser(z);
setCurrentPeopleView("person")
} else {
await navigator.clipboard.writeText(z.id);
toast("Copied ID.")
}
}}></Person>
})}
</>
}

View file

@ -0,0 +1,119 @@
import { useEffect, useRef, useState } from "preact/hooks";
import toast from "react-hot-toast";
import Button from "../../miniComponents/Button";
import Input from "../../miniComponents/Input";
import Person from "../../miniComponents/Person";
import { trty } from "../../treaty";
export default function ({ user }) {
const [accountState, setAccountState] = useState<undefined | {
isOnline: boolean,
rooms: string[],
name: string
}>(undefined)
const banDialogRef = useRef<HTMLDialogElement>()
const banPermanentRef = useRef<HTMLInputElement>()
const banReasonRef = useRef<HTMLInputElement>()
const banTimeRef = useRef<HTMLInputElement>()
const muteDialogRef = useRef<HTMLDialogElement>()
const mutePermanentRef = useRef<HTMLInputElement>()
const muteReasonRef = useRef<HTMLInputElement>()
const muteTimeRef = useRef<HTMLInputElement>()
useEffect(() => {
async function asyncFunc() {
const data = (await trty.api.staff.isUserOnline.post({
userId: user.id
})).data;
if (data) {
setAccountState(data as {
isOnline: boolean,
rooms: string[],
name: string
})
} else {
setAccountState(undefined)
}
}
asyncFunc()
}, [user])
return <>
<dialog class="bg-slate-400 p-5 rounded-xl" ref={banDialogRef}>
<div class="flex flex-col items-center" >
<Input placeholder="Reason" extraClass="mb-1" ref={banReasonRef}></Input>
<Input placeholder="Time (in MS)" extraClass="mb-1" type="number" ref={banTimeRef}></Input>
<div class="mb-2">
<Input type="checkbox" ref={banPermanentRef}></Input> is Permanent?
</div>
<Button onClick={() => {
banDialogRef.current.close();
const time = banTimeRef.current.value;
const reason = banReasonRef.current.value;
const isPermanent = banPermanentRef.current.checked;
toast.promise((async () => {
await trty.api.staff.ban.post({
id: user.id,
reason: reason,
ms: +time,
isPermanent: isPermanent,
})
})(), {
loading: "Banning..",
success: "Banned!",
error: "Failed to ban."
})
}}>Ban them</Button>
</div>
</dialog>
<dialog class="bg-slate-400 p-5 rounded-xl" ref={muteDialogRef}>
<div class="flex flex-col items-center" >
<Input placeholder="Reason" extraClass="mb-1" ref={muteReasonRef}></Input>
<Input placeholder="Time (in MS)" extraClass="mb-1" type="number" ref={muteTimeRef}></Input>
<div class="mb-2">
<Input type="checkbox" ref={mutePermanentRef}></Input> is Permanent?
</div>
<Button onClick={() => {
muteDialogRef.current.close();
const time = muteTimeRef.current.value;
const reason = muteReasonRef.current.value;
const isPermanent = mutePermanentRef.current.checked;
toast.promise((async () => {
await trty.api.staff.mute.post({
id: user.id,
reason: reason,
ms: +time,
isPermanent: isPermanent,
})
})(), {
loading: "Muting..",
success: "Muted!",
error: "Failed to mute."
})
}}>Mute them</Button>
</div>
</dialog>
{accountState ? <>
<Person user={{id: user.id,username:accountState.name}}></Person>
<p class="text-center text-black">This user is currently <strong>{accountState.isOnline ? "online" : "offline"}.</strong></p>
{accountState.isOnline ? <p class="text-center text-black">They are in the room/s <strong>{accountState.rooms.join(", ")}</strong></p> : ""}
<Button extraClass="font-bold" onClick={async () => {
await navigator.clipboard.writeText(user.id)
toast("Copied ID.")
}}>{user.id}<br></br>(click to copy)</Button>
<Button onClick={() => banDialogRef.current.showModal()}>Ban</Button>
<Button onClick={() => muteDialogRef.current.showModal()}>Mute</Button>
</> : "Loading.."}
</>
}

View file

@ -0,0 +1,188 @@
import { useEffect, useRef, useState } from "preact/hooks";
import toast from 'react-hot-toast';
import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
import Button from "../../miniComponents/Button";
import Input from "../../miniComponents/Input";
import { trty } from "../../treaty";
export default function SidebarAccount({ setSocketUrl, currentView, accountState, setAccountState }) {
const rerun = useState<boolean>(false);
useEffect(() => {
async function asyncFunc() {
const data = (await trty.api.auth.userInfo.get()).data;
if (data) {
setAccountState(data as { username: string })
} else {
setAccountState(undefined)
}
}
asyncFunc()
}, [rerun])
function AccountSignedIn() {
const statusRef = useRef<HTMLInputElement>();
async function handleLogout() {
await trty.api.auth.logout.get();
rerun[1](a => !a)
}
async function uploadImage() {
let image_input = document.getElementById("profile_picture_input") as HTMLInputElement
if (!image_input) {
image_input = document.createElement("input");
image_input.style.display = "none";
image_input.type = "file"
image_input.accept = "image/jpeg, image/png, image/jpg"
image_input.id = "profile_picture_input"
document.body.appendChild(image_input)
image_input.addEventListener("change", () => {
toast.promise((async () => {
await trty.api.profile.setProfilePicture.post({
file: image_input.files[0]
})
})(), {
loading: "Uploading..",
success: "Uploaded!",
error: "Upload failed. :("
})
})
}
image_input.click()
}
return (
<>
<div class="flex flex-col">
<h1 class="text-center text-xl text-white ">Signed in as <strong>{accountState.username}</strong></h1>
<Button onClick={handleLogout}>Logout</Button>
<Button onClick={uploadImage}>
<div class="flex items-center text-center">
<i class="fa-solid fa-image text-3xl text-slate-500 mr-2"></i>Set a profile picture
</div>
</Button>
<Input placeholder="Set a status!" ref={statusRef}></Input>
<Button onClick={() => {
toast.promise((async () => {
if(statusRef.current.value.trim()) {
await trty.api.profile.setStatus.post({
status: statusRef.current.value
});
}
})(), {
success: "Status set!",
loading: "Setting status..",
error: "Failed to set status."
})
}}>Set status</Button>
</div>
</>
)
}
function AccountSignedOut() {
const usernameRef = useRef<HTMLInputElement>();
const passwordRef = useRef<HTMLInputElement>();
const { executeRecaptcha } = useGoogleReCaptcha();
async function handleSignIn() {
const username = usernameRef.current.value.trim();
const password = passwordRef.current.value.trim();
if (!username || !password) {
toast.error("Missing username or password!", {
position: "bottom-right",
})
return;
}
const loadingToast = toast.loading("Logging in..", {
position: "bottom-right",
});
const token = await executeRecaptcha("login");
const request = await trty.api.auth.login.post({
password: password,
username: username,
recaptcha: token
})
if (request.status != 200) {
toast.error(request.error.value as unknown as string, {
position: "bottom-right",
});
toast.remove(loadingToast);
return;
}
toast.success(`You've logged into ${username}!`, {
position: "bottom-right",
})
toast.remove(loadingToast);
setSocketUrl(z => z.replace(/\?.*$/gm, "") + "?a=" + Math.random())
rerun[1](a => !a)
}
async function handleRegister() {
const username = usernameRef.current.value.trim();
const password = passwordRef.current.value.trim();
if (!username || !password) {
toast.error("Missing username or password!", {
position: "bottom-right",
})
return;
}
const loadingToast = toast.loading("Registering..", {
position: "bottom-right",
});
const token = await executeRecaptcha("register");
const request = await trty.api.auth.register.post({
password: password,
username: username,
recaptcha: token
})
if (request.status != 200) {
toast.error(request.error.value as unknown as string, {
position: "bottom-right",
});
toast.remove(loadingToast);
return;
}
toast.success(`Your account ${username} has been registered!`, {
position: "bottom-right",
})
toast.remove(loadingToast);
}
return (
<>
<div class="flex flex-col">
<Input placeholder="Username" extraClass="mb-1" type="username" ref={usernameRef}></Input>
<Input placeholder="Password" extraClass="mb-1" type="password" ref={passwordRef}></Input>
<Button onClick={handleSignIn}>Sign In</Button>
<Button onClick={handleRegister}>Register</Button>
<div class="text-sm m-2">
This site is protected by reCAPTCHA and the Google <a class="text-gray-400" href="https://policies.google.com/privacy">Privacy Policy</a> and <a href="https://policies.google.com/terms" class="text-gray-400">Terms of Service</a> apply.
</div>
</div>
</>
)
}
return (
<>
<h1 class="text-center text-3xl text-white ">Account</h1>
{
(() => {
if (!accountState) {
return <AccountSignedOut></AccountSignedOut>
} else {
return <AccountSignedIn></AccountSignedIn>
}
})()
}
</>
)
}

View file

@ -0,0 +1,60 @@
import { useRef } from "preact/hooks";
import Button from "../../miniComponents/Button";
import Input from "../../miniComponents/Input";
export default function SidebarRooms({ setSendRoomHidden, setRoomName, roomName, currentView, rooms }) {
const dialogRef = useRef<HTMLDialogElement>();
const roomInputRef = useRef<HTMLInputElement>();
const roomIsHiddenRef = useRef<HTMLInputElement>();
function createRoomDialog() {
dialogRef.current.showModal();
}
function createRoom() {
const input = roomInputRef.current.value.trim();
if (input) {
setSendRoomHidden(roomIsHiddenRef.current.checked);
localStorage.lastRoom = input;
setRoomName(input);
dialogRef.current.close();
}
}
return (
<>
<dialog ref={dialogRef} class="bg-slate-400 p-5 rounded-xl">
<div class="flex flex-col">
<Input ref={roomInputRef} placeholder="My Awsome Room"></Input>
<div class="text-center text-white my-2">
<Input type="checkbox" extraClasses="mr-2" ref={roomIsHiddenRef}></Input>
Is room hidden?
</div>
<Button onClick={createRoom}>Create room</Button>
</div>
</dialog>
<h1 class="text-center text-3xl text-white ">Rooms</h1>
<div class="flex flex-col">
{
rooms ?
rooms.map(z => {
return <Button onClick={
() => {
setSendRoomHidden(false);
setRoomName(z.name)
}
} extraClass={roomName == z.name ? "bg-green-200" : "bg-slate-400"}>
<div class="text-2xl">{z.name}</div>
<div>{z.count} pe{z.count == 1 ? "rson" : "ople"}</div>
</Button>
}) : <h1 class="text-2xl text-black text-center">Loading..</h1>
}
<Button onClick={createRoomDialog}>
Create your own room
</Button>
</div>
</>
)
}

View file

@ -0,0 +1,63 @@
import { useRef, useState } from "preact/hooks";
import toast from 'react-hot-toast';
import Button from "../../miniComponents/Button";
import Input from "../../miniComponents/Input";
import { trty } from "../../treaty";
export default function SidebarStaff({ setCurrentPeopleView, currentView, setUser }) {
const userIdRef = useRef<HTMLInputElement>();
const [punishments, setPunishments] = useState<undefined | any[]>();
return (
<>
<h1 class="text-center text-3xl text-white">Staff</h1>
<div class="border-b-4">
<Input placeholder="user ID" extraClass="m-0" ref={userIdRef}></Input>
<Button extraClass="m-0 py-2 my-2 w-full" onClick={async () => {
const req = await trty.api.staff.punishments.post({
userId: userIdRef.current.value
})
if (req.data) setPunishments(req.data as any);
}}>See punishments</Button>
<Button extraClass="m-0 py-2 mb-2 w-full" onClick={() => {
const bar = document.getElementById("peopleBar");
const barButton = document.getElementById("peopleBarButton");
if (bar.style.display !== "flex") {
bar.style.display = "flex";
barButton.className = barButton.className.replace('right-6', "right-64")
}
setCurrentPeopleView("person");
setUser({ id: userIdRef.current.value });
}}>User actions</Button>
</div>
{punishments === undefined ? "No user searched." : punishments.length == 0 ? "This user has no punishments!" : punishments.map((z, i) => {
return <div class={(i == 0 ? "" : "border-t-2") + " text-black"}>
<Button extraClass="p-2 m-1" onClick={() => {
toast.promise((async () => {
await trty.api.staff.invalidatePunishment.post({
punishmentId: z.id
})
})(), {
loading: "Invalidating punishment..",
success: "Invalidated.",
error: "Failed to invalidate!"
})
}}>Remove punishment</Button>
<div><i class={"text-white fa-solid fa-" + (z.type == "ban" ? "hammer-crash" : "comment-slash")}></i> {z.type.toUpperCase()}</div>
<div>ID: <strong>{z.id}</strong></div>
<div>Reason: <strong>{z.reason}</strong></div>
<div>Banned by: <strong>{z.staffId}</strong></div>
<div>Time: <strong>{z.time}ms</strong></div>
<div>Banned at: <strong>{z.at.toString()}</strong></div>
</div>
})}
</>
)
}