add basic profile modal

This commit is contained in:
Grant 2024-06-11 13:53:24 -06:00
parent 169c19b8e2
commit 9e2e0556c4
6 changed files with 101 additions and 5 deletions

View File

@ -14,6 +14,7 @@ import { AuthErrors } from "./AuthErrors";
import "../lib/keybinds"; import "../lib/keybinds";
import { PixelWhoisSidebar } from "./PixelWhoisSidebar"; import { PixelWhoisSidebar } from "./PixelWhoisSidebar";
import { KeybindModal } from "./KeybindModal"; import { KeybindModal } from "./KeybindModal";
import { ProfileModal } from "./Profile/ProfileModal";
const Chat = lazy(() => import("./Chat/Chat")); const Chat = lazy(() => import("./Chat/Chat"));
@ -142,6 +143,8 @@ const AppInner = () => {
<KeybindModal /> <KeybindModal />
<AuthErrors /> <AuthErrors />
<ProfileModal />
<ToastContainer position="top-left" /> <ToastContainer position="top-left" />
</> </>
); );

View File

@ -0,0 +1,50 @@
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>
);
};

View File

@ -1,4 +1,4 @@
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, Link, Spinner } from "@nextui-org/react"; import { Button, Link, Spinner } from "@nextui-org/react";
import { ClientConfig } from "@sc07-canvas/lib/src/net"; import { ClientConfig } from "@sc07-canvas/lib/src/net";
@ -6,7 +6,7 @@ import { MouseEvent, useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useAppContext } from "../../contexts/AppContext"; import { useAppContext } from "../../contexts/AppContext";
interface IUser { export interface IUser {
sub: string; sub: string;
display_name?: string; display_name?: string;
picture_url?: string; picture_url?: string;
@ -25,7 +25,7 @@ const getMatrixLink = (user: IUser, config: ClientConfig) => {
* @returns * @returns
*/ */
export const UserCard = ({ user }: { user: IUser }) => { export const UserCard = ({ user }: { user: IUser }) => {
const { config } = useAppContext(); const { config, setProfile } = useAppContext();
const [messageStatus, setMessageStatus] = useState< const [messageStatus, setMessageStatus] = useState<
"loading" | "no_account" | "has_account" | "error" "loading" | "no_account" | "has_account" | "error"
>("loading"); >("loading");
@ -68,6 +68,10 @@ export const UserCard = ({ user }: { user: IUser }) => {
} }
}; };
const openProfile = () => {
setProfile(user.sub);
};
return ( return (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
@ -101,7 +105,9 @@ export const UserCard = ({ user }: { user: IUser }) => {
)} )}
</div> </div>
</div> </div>
<Button size="sm">View Profile</Button> <Button size="sm" onPress={openProfile}>
View Profile
</Button>
</div> </div>
); );
}; };

View File

@ -13,15 +13,17 @@ import { api } from "../lib/utils";
interface IAppContext { interface IAppContext {
config?: ClientConfig; config?: ClientConfig;
user?: AuthSession; user?: AuthSession;
connected: boolean;
canvasPosition?: ICanvasPosition; canvasPosition?: ICanvasPosition;
setCanvasPosition: (v: ICanvasPosition) => void; setCanvasPosition: (v: ICanvasPosition) => void;
cursorPosition?: IPosition; cursorPosition?: IPosition;
setCursorPosition: (v?: IPosition) => void; setCursorPosition: (v?: IPosition) => void;
pixels: { available: number }; pixels: { available: number };
undo?: { available: true; expireAt: number }; undo?: { available: true; expireAt: number };
loadChat: boolean; loadChat: boolean;
setLoadChat: (v: boolean) => void; setLoadChat: (v: boolean) => void;
connected: boolean;
settingsSidebar: boolean; settingsSidebar: boolean;
setSettingsSidebar: (v: boolean) => void; setSettingsSidebar: (v: boolean) => void;
@ -35,6 +37,9 @@ interface IAppContext {
heatmapOverlay: IMapOverlay; heatmapOverlay: IMapOverlay;
setHeatmapOverlay: React.Dispatch<React.SetStateAction<IMapOverlay>>; setHeatmapOverlay: React.Dispatch<React.SetStateAction<IMapOverlay>>;
profile?: string; // sub
setProfile: (v?: string) => void;
hasAdmin: boolean; hasAdmin: boolean;
} }
@ -93,6 +98,8 @@ export const AppContext = ({ children }: PropsWithChildren) => {
loading: false, loading: false,
}); });
const [profile, setProfile] = useState<string>();
const [hasAdmin, setHasAdmin] = useState(false); const [hasAdmin, setHasAdmin] = useState(false);
useEffect(() => { useEffect(() => {
@ -195,6 +202,8 @@ export const AppContext = ({ children }: PropsWithChildren) => {
setBlankOverlay, setBlankOverlay,
heatmapOverlay, heatmapOverlay,
setHeatmapOverlay, setHeatmapOverlay,
profile,
setProfile,
}} }}
> >
{!config && ( {!config && (

View File

@ -1,3 +1,5 @@
import { toast } from "react-toastify";
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const api = async <T = unknown, Error = string>( export const api = async <T = unknown, Error = string>(
endpoint: string, endpoint: string,
@ -36,3 +38,10 @@ export const api = async <T = unknown, Error = string>(
export type EnforceObjectType<T> = <V extends { [k: string]: T }>( export type EnforceObjectType<T> = <V extends { [k: string]: T }>(
v: V v: V
) => { [k in keyof V]: T }; ) => { [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")
);
};

View File

@ -244,4 +244,23 @@ app.get("/heatmap", async (req, res) => {
res.json({ success: true, heatmap }); res.json({ success: true, heatmap });
}); });
app.get("/user/:sub", async (req, res) => {
const user = await prisma.user.findFirst({ where: { sub: req.params.sub } });
if (!user) {
return res.status(404).json({ success: false, error: "unknown_user" });
}
res.json({
success: true,
user: {
sub: user.sub,
display_name: user.display_name,
picture_url: user.picture_url,
profile_url: user.profile_url,
isAdmin: user.isAdmin,
isModerator: user.isModerator,
},
});
});
export default app; export default app;