Loading packages/client/src/components/App.tsx +3 −0 Original line number Diff line number Diff line Loading @@ -14,6 +14,7 @@ import { AuthErrors } from "./AuthErrors"; import "../lib/keybinds"; import { PixelWhoisSidebar } from "./PixelWhoisSidebar"; import { KeybindModal } from "./KeybindModal"; import { ProfileModal } from "./Profile/ProfileModal"; const Chat = lazy(() => import("./Chat/Chat")); Loading Loading @@ -142,6 +143,8 @@ const AppInner = () => { <KeybindModal /> <AuthErrors /> <ProfileModal /> <ToastContainer position="top-left" /> </> ); Loading packages/client/src/components/Profile/ProfileModal.tsx 0 → 100644 +50 −0 Original line number Diff line number Diff line import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, } from "@nextui-org/react"; import { useAppContext } from "../../contexts/AppContext"; import { useEffect, useState } from "react"; import { IUser, UserCard } from "./UserCard"; import { api, handleError } from "../../lib/utils"; export const ProfileModal = () => { const { profile, setProfile } = useAppContext(); const [user, setUser] = useState<IUser>(); useEffect(() => { if (!profile) { setUser(undefined); return; } api<{ user: IUser }>("/api/user/" + profile).then(({ status, data }) => { if (status === 200 && data.success) { setUser(data.user); } else { handleError({ status, data }); } }); }, [profile]); return ( <Modal isOpen={!!profile} onClose={() => setProfile()} placement="center"> <ModalContent> {(onClose) => ( <> <ModalHeader className="flex flex-col gap-1">Profile</ModalHeader> <ModalBody> {user ? <UserCard user={user} /> : <>Loading...</>} </ModalBody> <ModalFooter> <Button onPress={onClose}>Close</Button> </ModalFooter> </> )} </ModalContent> </Modal> ); }; packages/client/src/components/Profile/UserCard.tsx +10 −4 Original line number Diff line number Diff line import { faMessage, faWarning, faX } from "@fortawesome/free-solid-svg-icons"; import { faMessage, faWarning } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button, Link, Spinner } from "@nextui-org/react"; import { ClientConfig } from "@sc07-canvas/lib/src/net"; Loading @@ -6,7 +6,7 @@ import { MouseEvent, useEffect, useState } from "react"; import { toast } from "react-toastify"; import { useAppContext } from "../../contexts/AppContext"; interface IUser { export interface IUser { sub: string; display_name?: string; picture_url?: string; Loading @@ -25,7 +25,7 @@ const getMatrixLink = (user: IUser, config: ClientConfig) => { * @returns */ export const UserCard = ({ user }: { user: IUser }) => { const { config } = useAppContext(); const { config, setProfile } = useAppContext(); const [messageStatus, setMessageStatus] = useState< "loading" | "no_account" | "has_account" | "error" >("loading"); Loading Loading @@ -68,6 +68,10 @@ export const UserCard = ({ user }: { user: IUser }) => { } }; const openProfile = () => { setProfile(user.sub); }; return ( <div className="flex flex-col gap-1"> <div className="flex flex-row gap-2"> Loading Loading @@ -101,7 +105,9 @@ export const UserCard = ({ user }: { user: IUser }) => { )} </div> </div> <Button size="sm">View Profile</Button> <Button size="sm" onPress={openProfile}> View Profile </Button> </div> ); }; packages/client/src/contexts/AppContext.tsx +10 −1 Original line number Diff line number Diff line Loading @@ -13,15 +13,17 @@ import { api } from "../lib/utils"; interface IAppContext { config?: ClientConfig; user?: AuthSession; connected: boolean; canvasPosition?: ICanvasPosition; setCanvasPosition: (v: ICanvasPosition) => void; cursorPosition?: IPosition; setCursorPosition: (v?: IPosition) => void; pixels: { available: number }; undo?: { available: true; expireAt: number }; loadChat: boolean; setLoadChat: (v: boolean) => void; connected: boolean; settingsSidebar: boolean; setSettingsSidebar: (v: boolean) => void; Loading @@ -35,6 +37,9 @@ interface IAppContext { heatmapOverlay: IMapOverlay; setHeatmapOverlay: React.Dispatch<React.SetStateAction<IMapOverlay>>; profile?: string; // sub setProfile: (v?: string) => void; hasAdmin: boolean; } Loading Loading @@ -93,6 +98,8 @@ export const AppContext = ({ children }: PropsWithChildren) => { loading: false, }); const [profile, setProfile] = useState<string>(); const [hasAdmin, setHasAdmin] = useState(false); useEffect(() => { Loading Loading @@ -195,6 +202,8 @@ export const AppContext = ({ children }: PropsWithChildren) => { setBlankOverlay, heatmapOverlay, setHeatmapOverlay, profile, setProfile, }} > {!config && ( Loading packages/client/src/lib/utils.ts +9 −0 Original line number Diff line number Diff line import { toast } from "react-toastify"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const api = async <T = unknown, Error = string>( endpoint: string, Loading Loading @@ -36,3 +38,10 @@ export const api = async <T = unknown, Error = string>( export type EnforceObjectType<T> = <V extends { [k: string]: T }>( v: V ) => { [k in keyof V]: T }; export const handleError = (api_response: Awaited<ReturnType<typeof api>>) => { toast.error( `Error: [${api_response.status}] ` + ("error" in api_response.data ? api_response.data.error : "Unknown Error") ); }; Loading
packages/client/src/components/App.tsx +3 −0 Original line number Diff line number Diff line Loading @@ -14,6 +14,7 @@ import { AuthErrors } from "./AuthErrors"; import "../lib/keybinds"; import { PixelWhoisSidebar } from "./PixelWhoisSidebar"; import { KeybindModal } from "./KeybindModal"; import { ProfileModal } from "./Profile/ProfileModal"; const Chat = lazy(() => import("./Chat/Chat")); Loading Loading @@ -142,6 +143,8 @@ const AppInner = () => { <KeybindModal /> <AuthErrors /> <ProfileModal /> <ToastContainer position="top-left" /> </> ); Loading
packages/client/src/components/Profile/ProfileModal.tsx 0 → 100644 +50 −0 Original line number Diff line number Diff line import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, } from "@nextui-org/react"; import { useAppContext } from "../../contexts/AppContext"; import { useEffect, useState } from "react"; import { IUser, UserCard } from "./UserCard"; import { api, handleError } from "../../lib/utils"; export const ProfileModal = () => { const { profile, setProfile } = useAppContext(); const [user, setUser] = useState<IUser>(); useEffect(() => { if (!profile) { setUser(undefined); return; } api<{ user: IUser }>("/api/user/" + profile).then(({ status, data }) => { if (status === 200 && data.success) { setUser(data.user); } else { handleError({ status, data }); } }); }, [profile]); return ( <Modal isOpen={!!profile} onClose={() => setProfile()} placement="center"> <ModalContent> {(onClose) => ( <> <ModalHeader className="flex flex-col gap-1">Profile</ModalHeader> <ModalBody> {user ? <UserCard user={user} /> : <>Loading...</>} </ModalBody> <ModalFooter> <Button onPress={onClose}>Close</Button> </ModalFooter> </> )} </ModalContent> </Modal> ); };
packages/client/src/components/Profile/UserCard.tsx +10 −4 Original line number Diff line number Diff line import { faMessage, faWarning, faX } from "@fortawesome/free-solid-svg-icons"; import { faMessage, faWarning } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button, Link, Spinner } from "@nextui-org/react"; import { ClientConfig } from "@sc07-canvas/lib/src/net"; Loading @@ -6,7 +6,7 @@ import { MouseEvent, useEffect, useState } from "react"; import { toast } from "react-toastify"; import { useAppContext } from "../../contexts/AppContext"; interface IUser { export interface IUser { sub: string; display_name?: string; picture_url?: string; Loading @@ -25,7 +25,7 @@ const getMatrixLink = (user: IUser, config: ClientConfig) => { * @returns */ export const UserCard = ({ user }: { user: IUser }) => { const { config } = useAppContext(); const { config, setProfile } = useAppContext(); const [messageStatus, setMessageStatus] = useState< "loading" | "no_account" | "has_account" | "error" >("loading"); Loading Loading @@ -68,6 +68,10 @@ export const UserCard = ({ user }: { user: IUser }) => { } }; const openProfile = () => { setProfile(user.sub); }; return ( <div className="flex flex-col gap-1"> <div className="flex flex-row gap-2"> Loading Loading @@ -101,7 +105,9 @@ export const UserCard = ({ user }: { user: IUser }) => { )} </div> </div> <Button size="sm">View Profile</Button> <Button size="sm" onPress={openProfile}> View Profile </Button> </div> ); };
packages/client/src/contexts/AppContext.tsx +10 −1 Original line number Diff line number Diff line Loading @@ -13,15 +13,17 @@ import { api } from "../lib/utils"; interface IAppContext { config?: ClientConfig; user?: AuthSession; connected: boolean; canvasPosition?: ICanvasPosition; setCanvasPosition: (v: ICanvasPosition) => void; cursorPosition?: IPosition; setCursorPosition: (v?: IPosition) => void; pixels: { available: number }; undo?: { available: true; expireAt: number }; loadChat: boolean; setLoadChat: (v: boolean) => void; connected: boolean; settingsSidebar: boolean; setSettingsSidebar: (v: boolean) => void; Loading @@ -35,6 +37,9 @@ interface IAppContext { heatmapOverlay: IMapOverlay; setHeatmapOverlay: React.Dispatch<React.SetStateAction<IMapOverlay>>; profile?: string; // sub setProfile: (v?: string) => void; hasAdmin: boolean; } Loading Loading @@ -93,6 +98,8 @@ export const AppContext = ({ children }: PropsWithChildren) => { loading: false, }); const [profile, setProfile] = useState<string>(); const [hasAdmin, setHasAdmin] = useState(false); useEffect(() => { Loading Loading @@ -195,6 +202,8 @@ export const AppContext = ({ children }: PropsWithChildren) => { setBlankOverlay, heatmapOverlay, setHeatmapOverlay, profile, setProfile, }} > {!config && ( Loading
packages/client/src/lib/utils.ts +9 −0 Original line number Diff line number Diff line import { toast } from "react-toastify"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const api = async <T = unknown, Error = string>( endpoint: string, Loading Loading @@ -36,3 +38,10 @@ export const api = async <T = unknown, Error = string>( export type EnforceObjectType<T> = <V extends { [k: string]: T }>( v: V ) => { [k in keyof V]: T }; export const handleError = (api_response: Awaited<ReturnType<typeof api>>) => { toast.error( `Error: [${api_response.status}] ` + ("error" in api_response.data ? api_response.data.error : "Unknown Error") ); };