add basic profile modal
This commit is contained in:
parent
169c19b8e2
commit
9e2e0556c4
|
@ -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" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 && (
|
||||||
|
|
|
@ -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")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue