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 { PixelWhoisSidebar } from "./PixelWhoisSidebar";
import { KeybindModal } from "./KeybindModal";
import { ProfileModal } from "./Profile/ProfileModal";
const Chat = lazy(() => import("./Chat/Chat"));
@ -142,6 +143,8 @@ const AppInner = () => {
<KeybindModal />
<AuthErrors />
<ProfileModal />
<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 { Button, Link, Spinner } from "@nextui-org/react";
import { ClientConfig } from "@sc07-canvas/lib/src/net";
@ -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;
@ -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");
@ -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">
@ -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>
);
};

View File

@ -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;
@ -35,6 +37,9 @@ interface IAppContext {
heatmapOverlay: IMapOverlay;
setHeatmapOverlay: React.Dispatch<React.SetStateAction<IMapOverlay>>;
profile?: string; // sub
setProfile: (v?: string) => void;
hasAdmin: boolean;
}
@ -93,6 +98,8 @@ export const AppContext = ({ children }: PropsWithChildren) => {
loading: false,
});
const [profile, setProfile] = useState<string>();
const [hasAdmin, setHasAdmin] = useState(false);
useEffect(() => {
@ -195,6 +202,8 @@ export const AppContext = ({ children }: PropsWithChildren) => {
setBlankOverlay,
heatmapOverlay,
setHeatmapOverlay,
profile,
setProfile,
}}
>
{!config && (

View File

@ -1,3 +1,5 @@
import { toast } from "react-toastify";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const api = async <T = unknown, Error = string>(
endpoint: string,
@ -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")
);
};

View File

@ -244,4 +244,23 @@ app.get("/heatmap", async (req, res) => {
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;