initial commit
This commit is contained in:
commit
6f5a39c212
47 changed files with 3601 additions and 0 deletions
2
web/.gitignore
vendored
Normal file
2
web/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
dist
|
||||
BIN
web/bun.lockb
Executable file
BIN
web/bun.lockb
Executable file
Binary file not shown.
24
web/index.html
Normal file
24
web/index.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<title>Trillium</title>
|
||||
<script src="https://kit.fontawesome.com/f8dad62a29.js" crossorigin="anonymous"></script>
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg">
|
||||
|
||||
<style>
|
||||
.grecaptcha-badge {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app" class="dark"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
33
web/package.json
Normal file
33
web/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"private": true,
|
||||
"name": "web",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/eden": "^1.0.13",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"gif-picker-react": "^1.3.2",
|
||||
"preact": "^10.20.0",
|
||||
"react-google-recaptcha-v3": "^1.10.1",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-use-websocket": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "^2.8.2",
|
||||
"@types/react-google-recaptcha": "^2.1.9",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"vite": "^5.2.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "preact"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@fortawesome/fontawesome-free"
|
||||
]
|
||||
}
|
||||
6
web/postcss.config.js
Normal file
6
web/postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
20
web/public/logo.svg
Normal file
20
web/public/logo.svg
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 136 132" version="1.1">
|
||||
<path
|
||||
d="M 11.238283,77.871644 C 9.6359691,77.442305 7.2871274,85.365019 6.930159,89.268215 l 0.09375,12.531255 c 0,8.24363 -5.7560452,8.26497 -6.06249996,10.09375 -1.04518504,4.19882 7.06232836,3.71699 14.78969296,3.62907 15.092131,-0.17172 26.147807,-6.10331 26.147807,-7.94157 0,-4.69512 -2.384992,-4.84425 -3.625,-9.968755 -1.48773,-6.148245 -0.809437,-12.718987 -4.384679,-12.125 -3.029969,0.503396 -7.255084,-0.229024 -10.676538,-1.067985 -5.854236,-1.435495 -9.868194,-5.982978 -11.974409,-6.547336 z m 12.523856,20.133443 c -3.173125,3.366313 -3.280867,5.186763 -6.498471,4.006973 -2.574573,-0.94401 -0.80779,-3.617506 1.517064,-7.317657 1.953596,-3.529986 5.146861,-3.669661 6.792852,-2.880987 2.549064,1.221381 1.36168,2.825359 -1.811445,6.191671 z"
|
||||
style= "fill:rgb(229, 231, 235);fill-opacity:1;stroke:none" />
|
||||
<path
|
||||
d="M 71.371805,24.042853 C 72.38763,22.731426 65.990638,17.500276 62.529508,15.660996 59.019722,14.053831 55.668618,12.393772 50.643472,11.291727 43.268106,8.3286412 45.55116,2.4014777 43.975669,1.4036601 39.925542,-2.2851948 39.159793,4.383057 37.164116,14.324443 c -7.721778,14.391503 -7.065692,25.850088 -5.050114,26.4972 4.470374,1.435237 5.389738,-0.325068 10.594919,0.51411 6.245042,1.006824 11.256659,6.529478 12.092155,3.002848 0.708073,-2.988771 3.766673,-9.038409 6.194855,-11.590688 4.44462,-4.671769 9.45993,-6.897341 10.375874,-8.70506 z m -28.184982,2.686114 c -1.336242,-4.928584 -1.366585,-8.453652 0.317312,-8.904851 2.125118,-0.569423 2.863597,2.796957 4.552804,7.229228 2.206003,5.78828 5.035336,8.060584 3.604424,9.162751 -1.883975,1.451141 -7.138298,-2.558544 -8.47454,-7.487128 z"
|
||||
style= "fill:rgb(229, 231, 235);fill-opacity:1;stroke:none" />
|
||||
<path
|
||||
d="m 109.33641,55.830715 c -0.11034,0.01299 -0.21667,0.04389 -0.28125,0.125 -1.36236,1.710966 -3.1317,7.452006 -8.03125,11.03125 -2.844616,2.078062 -7.308728,5.293317 -10.266888,6.120006 -3.689801,1.031154 0.709128,5.964975 2.985019,11.960616 1.871085,4.929213 -1.929553,8.043403 1.753759,10.955004 2.347541,1.855695 16.29852,0.378895 25.38245,-6.113197 4.36016,-2.808938 12.49947,-1.182006 14.08497,-5.585759 1.5855,-4.403753 -8.57491,-1.080782 -11.87681,-8.52417 l -4.46499,-11.276874 c -1.86595,-3.165473 -7.62997,-8.886706 -9.28501,-8.691876 z m 8.52008,23.380046 c -0.36078,2.770582 -6.16422,-2.317005 -10.01431,-3.044842 -4.01357,-0.758742 -10.45728,2.584598 -8.586742,-2.132375 1.870542,-4.716974 19.733412,-2.512878 18.601052,5.177217 z m -11.69646,-0.396999 c 1.28677,-0.03378 3.47878,0.704378 4.61388,1.329449 4.5404,2.500282 1.87736,4.765754 0.65625,5.209998 -1.22111,0.444243 -1.65698,-0.80362 -5.15625,-1.741248 -6.24596,-1.6736 -3.97419,-4.696847 -0.11388,-4.798199 z"
|
||||
style= "fill:rgb(229, 231, 235);fill-opacity:1;stroke:none" />
|
||||
<path
|
||||
d="M 7.7923024,42.45318 C -2.0077829,65.105972 26.71296,89.696127 39.898909,78.861965 c 6.589995,-12.277406 13.55474,-12.860372 16.40625,-14.125 0.740344,-0.328338 1.139952,-4.761046 4.1875,-5.6875 C 57.283368,53.317038 47.93139,45.388652 36.511142,45.388652 c -5.46819,0 -7.887478,3.97375 -17.00789,4.223313 C 7.8224381,49.931588 9.3031945,41.562351 7.7923024,42.45318 z m 2.9503566,11.283785 c 1.336629,-2.315109 7.039156,3.971663 18.34375,1.78125 12.046196,-2.334108 19.104142,12.252633 14.34375,11.75 C 36.80316,65.420819 37.398401,60.17302 28.711409,60.705715 18.25717,62.45766 9.2181591,58.865339 10.742659,53.736965 z"
|
||||
style= "fill:rgb(100, 116, 139);fill-opacity:1;stroke:none" />
|
||||
<path
|
||||
d="m 92.02952,26.186975 c -9.125945,0 -15.511413,1.307991 -22.050533,5.416762 -8.050582,5.05848 -14.380046,13.510553 -10.330078,18.445728 6.01863,8.952383 12.21091,11.683583 13.15625,14.65625 1.024544,3.22173 -0.287197,5.399564 -0.915784,6.4616 -13.200216,8.806877 -11.472751,-2.915979 -16.802966,-1.36785 -12.231867,4.082598 -13.837875,15.230592 -11.0625,29.4375 1.175052,6.014995 6.848222,11.962175 11.40625,14.593755 4.558027,2.63158 12.26692,12.14989 8.53125,13.21875 -3.73567,1.06885 -3.913221,5.30111 0.34375,4.46875 4.256971,-0.83235 13.389958,-6.79471 18.84375,-14.0625 12.120315,-16.17878 6.988356,-45.114941 -3.21875,-44.218755 -1.946273,0.170884 -2.393987,-0.266654 -3.965837,-1.191472 -1.066665,-1.066666 0.17251,-3.962407 1.034428,-3.464778 8.075512,4.290847 17.606739,2.020003 23.77516,-7.125 3.27327,-4.852793 6.71183,-11.988102 4.84375,-20.34375 1.60329,-9.459235 9.33591,-5.755361 9.96875,-9.03125 -6.32119,-4.829512 -13.69944,-5.89374 -23.55689,-5.89374 z m -3.818111,6.67499 c 0.643748,0.03294 1.226158,0.288658 1.71875,0.78125 2.627157,2.627156 -5.109856,6.556107 -10.071252,11.517504 -4.961397,4.961396 -7.111819,13.097306 -10.5,11.03125 -3.282729,-2.001753 1.586049,-10.141425 7.375,-15.375 4.582,-4.142415 8.687926,-8.097743 11.477502,-7.955004 z m 6.40625,7.9375 c 3.813127,2.551966 -1.104534,4.705905 -3.026874,6.370624 -1.922341,1.664718 -4.058892,5.929572 -6.548754,3.481878 -4.041067,-7.397924 10.321718,-11.60946 9.575628,-9.852502 z m -1.625,10.65625 c 2.763568,-0.238557 1.756747,4.054721 0.28125,4.4375 -1.815996,0.471113 -5.426439,3.57716 -6.0625,2.5625 -0.636061,-1.01466 -1.540412,-3.676616 3.21875,-6 1.079909,-0.639475 1.924754,-0.944948 2.5625,-1 z m -23.65625,24.25 c 3.662476,0.02877 7.698919,5.806581 8.410935,18.673337 0.702975,12.703388 -1.093993,19.949208 -5.727143,22.322948 -5.408753,-0.577 1.232818,-5.83521 0.552172,-22.47298 -0.512636,-12.530912 -8.234635,-17.625405 -4.481727,-18.17982 0.116865,-0.01726 1.127619,-0.344413 1.245763,-0.343485 z m 11.5,4.78125 c 1.909349,-0.0278 3.33756,3.625376 3.78125,5.28125 0.764187,2.851984 1.679071,6.812796 1,10.4375 -0.795494,4.961315 -4.950395,1.748252 -4.804416,-4.203283 0.14198,-5.788552 -3.95109,-9.436322 -0.820584,-11.265467 0.290641,-0.16982 0.570986,-0.246028 0.84375,-0.25 z m -20.625,1.375 c 4.129893,0.3207 7.680609,16.713233 4.5625,20.875005 -0.235125,0.28828 -0.506327,0.52533 -0.78125,0.75 -3.897922,3.1854 -4.046108,-4.371026 -3.78125,-7.250005 0.313874,-3.411786 -3.81232,-12.40589 -0.40625,-14.34375 0.134237,-0.02406 0.273028,-0.0416 0.40625,-0.03125 z"
|
||||
style= "fill:rgb(100, 116, 139);fill-opacity:1;stroke:none" />
|
||||
<path
|
||||
d="m 62.878316,69.141267 c -1.73286,-1.09783 -1.666444,-3.322247 -0.516173,-5.196413 1.150271,-1.874167 2.816894,-1.773196 4.779929,-0.290318 1.963034,1.482877 1.193223,3.534175 0.274458,4.798645 -0.918765,1.264469 -2.805353,1.785916 -4.538214,0.688086 z"
|
||||
style= "fill:rgb(100, 116, 139);fill-opacity:1;stroke:none" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
132
web/src/components/App.tsx
Normal file
132
web/src/components/App.tsx
Normal 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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
79
web/src/components/Chat.tsx
Normal file
79
web/src/components/Chat.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
81
web/src/components/ChatBar.tsx
Normal file
81
web/src/components/ChatBar.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
76
web/src/components/People.tsx
Normal file
76
web/src/components/People.tsx
Normal 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></>;
|
||||
}
|
||||
55
web/src/components/Sidebar.tsx
Normal file
55
web/src/components/Sidebar.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
21
web/src/components/peopleViews/PeopleView.tsx
Normal file
21
web/src/components/peopleViews/PeopleView.tsx
Normal 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>
|
||||
})}
|
||||
</>
|
||||
}
|
||||
119
web/src/components/peopleViews/PersonView.tsx
Normal file
119
web/src/components/peopleViews/PersonView.tsx
Normal 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.."}
|
||||
</>
|
||||
}
|
||||
188
web/src/components/sidebarViews/SidebarAccount.tsx
Normal file
188
web/src/components/sidebarViews/SidebarAccount.tsx
Normal 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>
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
60
web/src/components/sidebarViews/SidebarRooms.tsx
Normal file
60
web/src/components/sidebarViews/SidebarRooms.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
63
web/src/components/sidebarViews/SidebarStaff.tsx
Normal file
63
web/src/components/sidebarViews/SidebarStaff.tsx
Normal 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>
|
||||
})}
|
||||
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
8
web/src/index.css
Normal file
8
web/src/index.css
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
35
web/src/index.tsx
Normal file
35
web/src/index.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
What if I never make it back from this damn panic attack?
|
||||
GTB sliding in a Hellcat bumping From First to Last
|
||||
When I die they're gonna make a park bench saying "This where he sat"
|
||||
Me and Yung Sherman going rehab, this shit is very sad
|
||||
Me and Yung Sherm in Venice Beach, man, this shit is very rad
|
||||
Me and Yung Sherman at the gym working out and getting tanned
|
||||
I never will see you again and I hope you understand
|
||||
I'm crashing down some like a wave over castles made of sand
|
||||
*/
|
||||
|
||||
import { render } from 'preact';
|
||||
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
|
||||
export function combineTailwind(first, second) {
|
||||
if(!second) return first;
|
||||
const secondSplit = second.split(" ")
|
||||
let classes = [];
|
||||
|
||||
const secondIds = secondSplit.map(z => z.split("-")[0]);
|
||||
|
||||
for(const defaultClass of first.split(" ")) {
|
||||
if(!secondIds.includes(defaultClass.split("-")[0])) {
|
||||
classes.push(defaultClass);
|
||||
}
|
||||
}
|
||||
|
||||
return classes.concat(secondSplit).join(" ")
|
||||
}
|
||||
|
||||
|
||||
import './index.css'
|
||||
import App from './components/App';
|
||||
|
||||
render(
|
||||
<GoogleReCaptchaProvider reCaptchaKey='6Lc8vOApAAAAAHAZlkLXSGe4Qe2J3gLMqRtodETV'><App /></GoogleReCaptchaProvider>, document.getElementById('app'));
|
||||
6
web/src/miniComponents/Button.tsx
Normal file
6
web/src/miniComponents/Button.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { combineTailwind } from "..";
|
||||
|
||||
export default function Button({onClick = undefined, extraClass = "", children}) {
|
||||
const combined = combineTailwind("bg-slate-200 text-black m-2 p-4 rounded-md", extraClass);
|
||||
return <button class={combined} onClick={onClick}>{children}</button>;
|
||||
}
|
||||
7
web/src/miniComponents/Input.tsx
Normal file
7
web/src/miniComponents/Input.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { ForwardedRef, forwardRef } from "preact/compat";
|
||||
import { combineTailwind } from "..";
|
||||
|
||||
export default forwardRef(function Input(props: any, ref: ForwardedRef<HTMLInputElement>) {
|
||||
const cssClass = combineTailwind("text-white bg-slate-500 rounded-md mx-2 mb-1 pl-2", props.extraClass)
|
||||
return <input class={cssClass} placeholder={props.placeholder} ref={ref} type={props.type} onKeyUp={props.onKeyUp}></input>;
|
||||
})
|
||||
34
web/src/miniComponents/Person.tsx
Normal file
34
web/src/miniComponents/Person.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import toast from "react-hot-toast";
|
||||
import { apiDomain } from "../treaty";
|
||||
|
||||
export async function copyId(id) {
|
||||
await navigator.clipboard.writeText(id);
|
||||
toast("Copied ID.")
|
||||
}
|
||||
|
||||
export default function Person({ onClick = undefined, user, time = undefined, viewStatus = false }) {
|
||||
if (!onClick) onClick = _ => copyId(user.id);
|
||||
let hasEmoji = "";
|
||||
let status = "";
|
||||
|
||||
if (user.status && viewStatus) {
|
||||
status = user.status;
|
||||
const segmented = user.status.split(" ");
|
||||
const isFirstEmoji = /\p{Emoji_Presentation}/gu.test(segmented[0])
|
||||
if (isFirstEmoji) {
|
||||
hasEmoji = segmented[0]
|
||||
status = segmented.slice(1).join(" ");
|
||||
}
|
||||
}
|
||||
return <button class="flex text-black flex-col m-2" onClick={onClick}>
|
||||
<div class="flex-row flex items-center">
|
||||
<img src={apiDomain + "/pfps/" + user.id + "?r=" + user.r} class="w-12 h-12 rounded-full" onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = "https://placehold.co/400";
|
||||
}}></img>
|
||||
<div class="text-2xl ml-2">{user.username}</div>
|
||||
{time ? <span class="ml-2 text-slate-500 text-sm">{new Date(time).toUTCString()}</span> : ""}
|
||||
</div>
|
||||
{user.status && viewStatus ?
|
||||
<div class="text-sm text-left">{hasEmoji ? <span class="text-xl">{hasEmoji} </span> : ""}{status}</div> : ""}
|
||||
</button>
|
||||
}
|
||||
15
web/src/treaty.tsx
Normal file
15
web/src/treaty.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
import { treaty } from '@elysiajs/eden'
|
||||
|
||||
import type { App } from '../../server/src/index'
|
||||
export let apiDomain = 'https://api.chat.sad.ovh';
|
||||
|
||||
if(location.hostname == "127.0.0.1" || location.hostname == "localhost") {
|
||||
apiDomain = "http://localhost:8001"
|
||||
}
|
||||
//@ts-ignore
|
||||
export const trty = treaty<App>(apiDomain, {
|
||||
fetch: {
|
||||
credentials: "include"
|
||||
}
|
||||
});
|
||||
11
web/tailwind.config.js
Normal file
11
web/tailwind.config.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
darkMode: "class",
|
||||
plugins: [],
|
||||
}
|
||||
20
web/tsconfig.json
Normal file
20
web/tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"noEmit": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"noErrorTruncation": true,
|
||||
/* Preact Config */
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"react": ["./node_modules/preact/compat/"],
|
||||
"react-dom": ["./node_modules/preact/compat/"]
|
||||
}
|
||||
},
|
||||
"include": ["node_modules/vite/client.d.ts", "**/*"]
|
||||
}
|
||||
7
web/vite.config.ts
Normal file
7
web/vite.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from "vite";
|
||||
import preact from "@preact/preset-vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [preact()],
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue