From b78e7a324df4772bfabff7c481c6b781f5870f66 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Thu, 26 Dec 2024 19:33:56 -0500 Subject: [PATCH] place holder landing pages --- server/routers/external.ts | 2 + server/routers/org/getOrgOverview.ts | 148 ++++++++++++++++ server/routers/org/index.ts | 3 +- .../components/OrganizationLandingCard.tsx | 102 +++++++++++ src/app/[orgId]/layout.tsx | 8 +- src/app/[orgId]/page.tsx | 61 ++++++- .../[orgId]/settings/access/roles/page.tsx | 22 +-- .../[orgId]/settings/access/users/page.tsx | 20 +-- src/app/[orgId]/settings/layout.tsx | 4 +- src/app/[orgId]/settings/resources/page.tsx | 10 +- src/app/[orgId]/settings/share-links/page.tsx | 4 +- src/app/[orgId]/settings/sites/page.tsx | 8 +- src/app/auth/layout.tsx | 17 +- src/app/components/OrganizationLanding.tsx | 97 +++++++++++ src/app/layout.tsx | 3 - src/app/not-found.tsx | 6 +- src/app/page.tsx | 52 +++--- src/app/setup/layout.tsx | 18 +- src/components/DataTablePagination.tsx | 4 +- src/components/Header.tsx | 155 +---------------- src/components/ProfileIcon.tsx | 158 ++++++++++++++++++ src/components/ui/card.tsx | 2 +- 22 files changed, 669 insertions(+), 235 deletions(-) create mode 100644 server/routers/org/getOrgOverview.ts create mode 100644 src/app/[orgId]/components/OrganizationLandingCard.tsx create mode 100644 src/app/components/OrganizationLanding.tsx create mode 100644 src/components/ProfileIcon.tsx diff --git a/server/routers/external.ts b/server/routers/external.ts index ecc2bb11..ba3743f5 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -336,6 +336,8 @@ authenticated.get( accessToken.listAccessTokens ); +authenticated.get(`/org/:orgId/overview`, verifyOrgAccess, org.getOrgOverview); + unauthenticated.get("/resource/:resourceId/auth", resource.getResourceAuthInfo); // authenticated.get( diff --git a/server/routers/org/getOrgOverview.ts b/server/routers/org/getOrgOverview.ts new file mode 100644 index 00000000..2a9b6f1a --- /dev/null +++ b/server/routers/org/getOrgOverview.ts @@ -0,0 +1,148 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { + orgs, + resources, + roles, + sites, + userOrgs, + userResources, + users, + userSites +} from "@server/db/schema"; +import { and, count, eq, inArray } from "drizzle-orm"; +import response from "@server/utils/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; + +const getOrgParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +export type GetOrgOverviewResponse = { + orgName: string; + orgId: string; + userRoleName: string; + numSites: number; + numUsers: number; + numResources: number; + isAdmin: boolean; + isOwner: boolean; +}; + +export async function getOrgOverview( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = getOrgParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + parsedParams.error.errors.map((e) => e.message).join(", ") + ) + ); + } + + const { orgId } = parsedParams.data; + + const org = await db + .select() + .from(orgs) + .where(eq(orgs.orgId, orgId)) + .limit(1); + + if (org.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Organization with ID ${orgId} not found` + ) + ); + } + + if (!req.userOrg) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") + ); + } + + const allSiteIds = await db + .select({ + siteId: sites.siteId + }) + .from(sites) + .where(eq(sites.orgId, orgId)); + + const [{ numSites }] = await db + .select({ numSites: count() }) + .from(userSites) + .where( + and( + eq(userSites.userId, req.userOrg.userId), + inArray( + userSites.siteId, + allSiteIds.map((site) => site.siteId) + ) + ) + ); + + const allResourceIds = await db + .select({ + resourceId: resources.resourceId + }) + .from(resources) + .where(eq(resources.orgId, orgId)); + + const [{ numResources }] = await db + .select({ numResources: count() }) + .from(userResources) + .where( + and( + eq(userResources.userId, req.userOrg.userId), + inArray( + userResources.resourceId, + allResourceIds.map((resource) => resource.resourceId) + ) + ) + ); + + const [{ numUsers }] = await db + .select({ numUsers: count() }) + .from(userOrgs) + .where(eq(userOrgs.orgId, orgId)); + + const [role] = await db + .select() + .from(roles) + .where(eq(roles.roleId, req.userOrg.roleId)); + + return response(res, { + data: { + orgName: org[0].name, + orgId: org[0].orgId, + userRoleName: role.name, + numSites, + numUsers, + numResources, + isAdmin: role.name === "Admin", + isOwner: req.userOrg?.isOwner || false + }, + success: true, + error: false, + message: "Organization overview retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/org/index.ts b/server/routers/org/index.ts index 2e904425..04ff1362 100644 --- a/server/routers/org/index.ts +++ b/server/routers/org/index.ts @@ -3,4 +3,5 @@ export * from "./createOrg"; export * from "./deleteOrg"; export * from "./updateOrg"; export * from "./listOrgs"; -export * from "./checkId"; \ No newline at end of file +export * from "./checkId"; +export * from "./getOrgOverview"; diff --git a/src/app/[orgId]/components/OrganizationLandingCard.tsx b/src/app/[orgId]/components/OrganizationLandingCard.tsx new file mode 100644 index 00000000..baffc09d --- /dev/null +++ b/src/app/[orgId]/components/OrganizationLandingCard.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { + Card, + CardHeader, + CardTitle, + CardContent, + CardFooter +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Users, Globe, Database, Cog, Settings, Waypoints, Combine } from "lucide-react"; + +interface OrgStat { + label: string; + value: number; + icon: React.ReactNode; +} + +type OrganizationLandingCardProps = { + overview: { + orgName: string; + stats: { + sites: number; + resources: number; + users: number; + }; + userRole: string; + isAdmin: boolean; + isOwner: boolean; + orgId: string; + }; +}; + +export default function OrganizationLandingCard( + props: OrganizationLandingCardProps +) { + const [orgData] = useState(props); + + const orgStats: OrgStat[] = [ + { + label: "Sites", + value: orgData.overview.stats.sites, + icon: + }, + { + label: "Resources", + value: orgData.overview.stats.resources, + icon: + }, + { + label: "Users", + value: orgData.overview.stats.users, + icon: + } + ]; + + return ( + + + + {orgData.overview.orgName} + + + +
+ {orgStats.map((stat, index) => ( +
+ {stat.icon} + + {stat.value} + + + {stat.label} + +
+ ))} +
+
+ Your role:{" "} + + {orgData.overview.isOwner ? "Owner" : orgData.overview.userRole} + +
+
+ {orgData.overview.isAdmin && ( + + + + + + )} +
+ ); +} diff --git a/src/app/[orgId]/layout.tsx b/src/app/[orgId]/layout.tsx index 55b83bac..e705eff3 100644 --- a/src/app/[orgId]/layout.tsx +++ b/src/app/[orgId]/layout.tsx @@ -1,6 +1,8 @@ import { internal } from "@app/api"; import { authCookieHeader } from "@app/api/cookies"; +import ProfileIcon from "@app/components/ProfileIcon"; import { verifySession } from "@app/lib/auth/verifySession"; +import UserProvider from "@app/providers/UserProvider"; import { GetOrgResponse } from "@server/routers/org"; import { GetOrgUserResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; @@ -47,5 +49,9 @@ export default async function OrgLayout(props: { redirect(`/`); } - return <>{props.children}; + return ( + <> + {props.children} + + ); } diff --git a/src/app/[orgId]/page.tsx b/src/app/[orgId]/page.tsx index cb3297d3..ff435ebd 100644 --- a/src/app/[orgId]/page.tsx +++ b/src/app/[orgId]/page.tsx @@ -1,10 +1,13 @@ -import { internal } from "@app/api"; -import { authCookieHeader } from "@app/api/cookies"; +import ProfileIcon from "@app/components/ProfileIcon"; import { verifySession } from "@app/lib/auth/verifySession"; -import { GetOrgUserResponse } from "@server/routers/user"; -import { AxiosResponse } from "axios"; -import { redirect } from "next/navigation"; +import UserProvider from "@app/providers/UserProvider"; import { cache } from "react"; +import OrganizationLandingCard from "./components/OrganizationLandingCard"; +import { GetOrgOverviewResponse } from "@server/routers/org/getOrgOverview"; +import { internal } from "@app/api"; +import { AxiosResponse } from "axios"; +import { authCookieHeader } from "@app/api/cookies"; +import { redirect } from "next/navigation"; type OrgPageProps = { params: Promise<{ orgId: string }>; @@ -14,9 +17,55 @@ export default async function OrgPage(props: OrgPageProps) { const params = await props.params; const orgId = params.orgId; + const getUser = cache(verifySession); + const user = await getUser(); + + let redirectToSettings = false; + let overview: GetOrgOverviewResponse | undefined; + try { + const res = await internal.get>( + `/org/${orgId}/overview`, + await authCookieHeader() + ); + overview = res.data.data; + + if (overview.isAdmin || overview.isOwner) { + redirectToSettings = true; + } + } catch (e) {} + + if (redirectToSettings) { + redirect(`/${orgId}/settings`); + } + return ( <> -

Welcome to {orgId} dashboard

+
+ {user && ( + + + + )} + + {overview && ( +
+ +
+ )} +
); } diff --git a/src/app/[orgId]/settings/access/roles/page.tsx b/src/app/[orgId]/settings/access/roles/page.tsx index 0a665e28..6e0b6783 100644 --- a/src/app/[orgId]/settings/access/roles/page.tsx +++ b/src/app/[orgId]/settings/access/roles/page.tsx @@ -18,13 +18,10 @@ export default async function RolesPage(props: RolesPageProps) { let roles: ListRolesResponse["roles"] = []; const res = await internal - .get>( - `/org/${params.orgId}/roles`, - await authCookieHeader() - ) - .catch((e) => { - console.error(e); - }); + .get< + AxiosResponse + >(`/org/${params.orgId}/roles`, await authCookieHeader()) + .catch((e) => {}); if (res && res.status === 200) { roles = res.data.data.roles; @@ -33,13 +30,10 @@ export default async function RolesPage(props: RolesPageProps) { let org: GetOrgResponse | null = null; const getOrg = cache(async () => internal - .get>( - `/org/${params.orgId}`, - await authCookieHeader() - ) - .catch((e) => { - console.error(e); - }) + .get< + AxiosResponse + >(`/org/${params.orgId}`, await authCookieHeader()) + .catch((e) => {}) ); const orgRes = await getOrg(); diff --git a/src/app/[orgId]/settings/access/users/page.tsx b/src/app/[orgId]/settings/access/users/page.tsx index 4c3ffe3b..37354c91 100644 --- a/src/app/[orgId]/settings/access/users/page.tsx +++ b/src/app/[orgId]/settings/access/users/page.tsx @@ -23,13 +23,10 @@ export default async function UsersPage(props: UsersPageProps) { let users: ListUsersResponse["users"] = []; const res = await internal - .get>( - `/org/${params.orgId}/users`, - await authCookieHeader() - ) - .catch((e) => { - console.error(e); - }); + .get< + AxiosResponse + >(`/org/${params.orgId}/users`, await authCookieHeader()) + .catch((e) => {}); if (res && res.status === 200) { users = res.data.data.users; @@ -38,10 +35,9 @@ export default async function UsersPage(props: UsersPageProps) { let org: GetOrgResponse | null = null; const getOrg = cache(async () => internal - .get>( - `/org/${params.orgId}`, - await authCookieHeader() - ) + .get< + AxiosResponse + >(`/org/${params.orgId}`, await authCookieHeader()) .catch((e) => { console.error(e); }) @@ -58,7 +54,7 @@ export default async function UsersPage(props: UsersPageProps) { email: user.email, status: "Confirmed", role: user.isOwner ? "Owner" : user.roleName || "Member", - isOwner: user.isOwner || false, + isOwner: user.isOwner || false }; }); diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx index c1793a5e..076d499e 100644 --- a/src/app/[orgId]/settings/layout.tsx +++ b/src/app/[orgId]/settings/layout.tsx @@ -91,9 +91,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { if (res && res.data.data.orgs) { orgs = res.data.data.orgs; } - } catch (e) { - console.error("Error fetching orgs", e); - } + } catch (e) {} return ( <> diff --git a/src/app/[orgId]/settings/resources/page.tsx b/src/app/[orgId]/settings/resources/page.tsx index ffafd1ac..3101508a 100644 --- a/src/app/[orgId]/settings/resources/page.tsx +++ b/src/app/[orgId]/settings/resources/page.tsx @@ -19,20 +19,18 @@ export default async function ResourcesPage(props: ResourcesPageProps) { try { const res = await internal.get>( `/org/${params.orgId}/resources`, - await authCookieHeader(), + await authCookieHeader() ); resources = res.data.data.resources; - } catch (e) { - console.error("Error fetching resources", e); - } + } catch (e) {} let org = null; try { const getOrg = cache(async () => internal.get>( `/org/${params.orgId}`, - await authCookieHeader(), - ), + await authCookieHeader() + ) ); const res = await getOrg(); org = res.data.data; diff --git a/src/app/[orgId]/settings/share-links/page.tsx b/src/app/[orgId]/settings/share-links/page.tsx index 21c562ac..73e6c7d2 100644 --- a/src/app/[orgId]/settings/share-links/page.tsx +++ b/src/app/[orgId]/settings/share-links/page.tsx @@ -24,9 +24,7 @@ export default async function ShareLinksPage(props: ShareLinksPageProps) { await authCookieHeader() ); tokens = res.data.data.accessTokens; - } catch (e) { - console.error("Error fetching tokens", e); - } + } catch (e) {} let org = null; try { diff --git a/src/app/[orgId]/settings/sites/page.tsx b/src/app/[orgId]/settings/sites/page.tsx index 6e7b2d37..0fa6ff50 100644 --- a/src/app/[orgId]/settings/sites/page.tsx +++ b/src/app/[orgId]/settings/sites/page.tsx @@ -15,12 +15,10 @@ export default async function SitesPage(props: SitesPageProps) { try { const res = await internal.get>( `/org/${params.orgId}/sites`, - await authCookieHeader(), + await authCookieHeader() ); sites = res.data.data.sites; - } catch (e) { - console.error("Error fetching sites", e); - } + } catch (e) {} function formatSize(mb: number): string { if (mb >= 1024 * 1024) { @@ -41,7 +39,7 @@ export default async function SitesPage(props: SitesPageProps) { mbOut: formatSize(site.megabytesOut || 0), orgId: params.orgId, type: site.type as any, - online: site.online, + online: site.online }; }); diff --git a/src/app/auth/layout.tsx b/src/app/auth/layout.tsx index f7efced2..58af7310 100644 --- a/src/app/auth/layout.tsx +++ b/src/app/auth/layout.tsx @@ -1,8 +1,12 @@ +import ProfileIcon from "@app/components/ProfileIcon"; +import { verifySession } from "@app/lib/auth/verifySession"; +import UserProvider from "@app/providers/UserProvider"; import { Metadata } from "next"; +import { cache } from "react"; export const metadata: Metadata = { title: `Auth - Pangolin`, - description: "", + description: "" }; type AuthLayoutProps = { @@ -10,8 +14,19 @@ type AuthLayoutProps = { }; export default async function AuthLayout({ children }: AuthLayoutProps) { + const getUser = cache(verifySession); + const user = await getUser(); + return ( <> + {user && ( + +
+ +
+
+ )} +
{children}
diff --git a/src/app/components/OrganizationLanding.tsx b/src/app/components/OrganizationLanding.tsx new file mode 100644 index 00000000..58e765e6 --- /dev/null +++ b/src/app/components/OrganizationLanding.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { useState } from "react"; +import { + Card, + CardHeader, + CardTitle, + CardContent, + CardDescription +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; +import { ArrowRight, Plus } from "lucide-react"; +interface Organization { + id: string; + name: string; +} + +interface OrganizationLandingProps { + organizations?: Organization[]; + disableCreateOrg?: boolean; +} + +export default function OrganizationLanding({ + organizations = [], + disableCreateOrg = false +}: OrganizationLandingProps) { + const [selectedOrg, setSelectedOrg] = useState(null); + + const handleOrgClick = (orgId: string) => { + setSelectedOrg(orgId); + }; + + function getDescriptionText() { + if (organizations.length === 0) { + if (!disableCreateOrg) { + return "You are not currently a member of any organizations. Create an organization to get started."; + } else { + return "You are not currently a member of any organizations."; + } + } + + return `You're a member of ${organizations.length} ${ + organizations.length === 1 ? "organization" : "organizations" + }.`; + } + + return ( + + + Welcome to Pangolin + {getDescriptionText()} + + + {organizations.length === 0 ? ( + disableCreateOrg ? ( +

+ You are not currently a member of any organizations. +

+ ) : ( + + + + ) + ) : ( +
    + {organizations.map((org) => ( +
  • + + + +
  • + ))} +
+ )} +
+
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6ad04643..959a5785 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -24,9 +24,6 @@ export default async function RootLayout({ }>) { const version = process.env.APP_VERSION; - const getUser = cache(verifySession); - const user = await getUser(); - return ( diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index eb0aea84..cb831311 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -3,11 +3,11 @@ import Link from "next/link"; export default async function NotFound() { return (
-

404

-

+

404

+

Page Not Found

-

+

Oops! The page you're looking for doesn't exist.

diff --git a/src/app/page.tsx b/src/app/page.tsx index 501c754c..a8afc214 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,6 @@ import { internal } from "@app/api"; import { authCookieHeader } from "@app/api/cookies"; +import ProfileIcon from "@app/components/ProfileIcon"; import { verifySession } from "@app/lib/auth/verifySession"; import UserProvider from "@app/providers/UserProvider"; import { ListOrgsResponse } from "@server/routers/org"; @@ -8,11 +9,15 @@ import { ArrowUpRight } from "lucide-react"; import Link from "next/link"; import { redirect } from "next/navigation"; import { cache } from "react"; +import OrganizationLanding from "./components/OrganizationLanding"; export const dynamic = "force-dynamic"; export default async function Page(props: { - searchParams: Promise<{ redirect: string | undefined, t: string | undefined }>; + searchParams: Promise<{ + redirect: string | undefined; + t: string | undefined; + }>; }) { const params = await props.searchParams; // this is needed to prevent static optimization @@ -42,39 +47,42 @@ export default async function Page(props: { try { const res = await internal.get>( `/orgs`, - await authCookieHeader(), + await authCookieHeader() ); if (res && res.data.data.orgs) { orgs = res.data.data.orgs; } - } catch (e) { - console.error(e); - } + } catch (e) {} if (!orgs.length) { - redirect("/setup"); + if ( + process.env.DISABLE_USER_CREATE_ORG === "false" || + user.serverAdmin + ) { + redirect("/setup"); + } } return ( <> - -

Logged in as {user.email}

-
- -
- {orgs.map((org) => ( - -
- {org.name} - +
+ {user && ( + +
+
- - ))} +
+ )} + +
+ ({ + name: org.name, + id: org.orgId + }))} + /> +
); diff --git a/src/app/setup/layout.tsx b/src/app/setup/layout.tsx index 0e32ee0a..55cf3242 100644 --- a/src/app/setup/layout.tsx +++ b/src/app/setup/layout.tsx @@ -1,4 +1,6 @@ +import ProfileIcon from "@app/components/ProfileIcon"; import { verifySession } from "@app/lib/auth/verifySession"; +import UserProvider from "@app/providers/UserProvider"; import { Metadata } from "next"; import { redirect } from "next/navigation"; import { cache } from "react"; @@ -29,6 +31,20 @@ export default async function SetupLayout({ } return ( -
{children}
+ <> +
+ {user && ( + +
+ +
+
+ )} + +
+ {children} +
+
+ ); } diff --git a/src/components/DataTablePagination.tsx b/src/components/DataTablePagination.tsx index 8dfc1c6a..3ea08d7c 100644 --- a/src/components/DataTablePagination.tsx +++ b/src/components/DataTablePagination.tsx @@ -47,8 +47,8 @@ export function DataTablePagination({
-
-
+
+
Page {table.getState().pagination.pageIndex + 1} of{" "} {table.getPageCount()}
diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 88faebde..19b3340c 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,7 +1,5 @@ "use client"; -import { createApiClient } from "@app/api"; -import { Avatar, AvatarFallback } from "@app/components/ui/avatar"; import { Button } from "@app/components/ui/button"; import { Command, @@ -12,39 +10,20 @@ import { CommandList, CommandSeparator } from "@app/components/ui/command"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger -} from "@app/components/ui/dropdown-menu"; import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useToast } from "@app/hooks/useToast"; -import { cn, formatAxiosError } from "@app/lib/utils"; +import { cn } from "@app/lib/utils"; import { ListOrgsResponse } from "@server/routers/org"; -import { - Check, - ChevronsUpDown, - Laptop, - LogOut, - Moon, - Plus, - Sun -} from "lucide-react"; -import { useTheme } from "next-themes"; +import { Check, ChevronsUpDown, Plus } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState } from "react"; -import Enable2FaForm from "./Enable2FaForm"; import { useUserContext } from "@app/hooks/useUserContext"; -import Disable2FaForm from "./Disable2FaForm"; +import ProfileIcon from "./ProfileIcon"; type HeaderProps = { orgId?: string; @@ -52,144 +31,18 @@ type HeaderProps = { }; export function Header({ orgId, orgs }: HeaderProps) { - const { toast } = useToast(); - const { setTheme, theme } = useTheme(); - const { user, updateUser } = useUserContext(); const [open, setOpen] = useState(false); - const [userTheme, setUserTheme] = useState<"light" | "dark" | "system">( - theme as "light" | "dark" | "system" - ); - - const [openEnable2fa, setOpenEnable2fa] = useState(false); - const [openDisable2fa, setOpenDisable2fa] = useState(false); const router = useRouter(); const { env } = useEnvContext(); - const api = createApiClient({ env }); - - function getInitials() { - return user.email.substring(0, 2).toUpperCase(); - } - - function logout() { - api.post("/auth/logout") - .catch((e) => { - console.error("Error logging out", e); - toast({ - title: "Error logging out", - description: formatAxiosError(e, "Error logging out") - }); - }) - .then(() => { - router.push("/auth/login"); - }); - } - - function handleThemeChange(theme: "light" | "dark" | "system") { - setUserTheme(theme); - setTheme(theme); - } - return ( <> - - -
-
- - - - - - -
-

- Signed in as -

-

- {user.email} -

-
- {user.serverAdmin && ( -

- Server Admin -

- )} -
- - {!user.twoFactorEnabled && ( - setOpenEnable2fa(true)} - > - Enable Two-factor - - )} - {user.twoFactorEnabled && ( - setOpenDisable2fa(true)} - > - Disable Two-factor - - )} - - Theme - {(["light", "dark", "system"] as const).map( - (themeOption) => ( - - handleThemeChange(themeOption) - } - > - {themeOption === "light" && ( - - )} - {themeOption === "dark" && ( - - )} - {themeOption === "system" && ( - - )} - - {themeOption} - - {userTheme === themeOption && ( - - - - )} - - ) - )} - - logout()}> - - Log out - -
-
- - {user.email} - -
+
diff --git a/src/components/ProfileIcon.tsx b/src/components/ProfileIcon.tsx new file mode 100644 index 00000000..2ac575c7 --- /dev/null +++ b/src/components/ProfileIcon.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { createApiClient } from "@app/api"; +import { Avatar, AvatarFallback } from "@app/components/ui/avatar"; +import { Button } from "@app/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger +} from "@app/components/ui/dropdown-menu"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useToast } from "@app/hooks/useToast"; +import { formatAxiosError } from "@app/lib/utils"; +import { Laptop, LogOut, Moon, Sun } from "lucide-react"; +import { useTheme } from "next-themes"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useUserContext } from "@app/hooks/useUserContext"; +import Disable2FaForm from "./Disable2FaForm"; +import Enable2FaForm from "./Enable2FaForm"; + +export default function ProfileIcon() { + const { toast } = useToast(); + const { setTheme, theme } = useTheme(); + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const { user, updateUser } = useUserContext(); + const router = useRouter(); + + const [userTheme, setUserTheme] = useState<"light" | "dark" | "system">( + theme as "light" | "dark" | "system" + ); + + const [openEnable2fa, setOpenEnable2fa] = useState(false); + const [openDisable2fa, setOpenDisable2fa] = useState(false); + + function getInitials() { + return user.email.substring(0, 2).toUpperCase(); + } + + function handleThemeChange(theme: "light" | "dark" | "system") { + setUserTheme(theme); + setTheme(theme); + } + + function logout() { + api.post("/auth/logout") + .catch((e) => { + console.error("Error logging out", e); + toast({ + title: "Error logging out", + description: formatAxiosError(e, "Error logging out") + }); + }) + .then(() => { + router.push("/auth/login"); + }); + } + + return ( + <> + + + +
+ + + + + + +
+

+ Signed in as +

+

+ {user.email} +

+
+ {user.serverAdmin && ( +

+ Server Admin +

+ )} +
+ + {!user.twoFactorEnabled && ( + setOpenEnable2fa(true)} + > + Enable Two-factor + + )} + {user.twoFactorEnabled && ( + setOpenDisable2fa(true)} + > + Disable Two-factor + + )} + + Theme + {(["light", "dark", "system"] as const).map( + (themeOption) => ( + + handleThemeChange(themeOption) + } + > + {themeOption === "light" && ( + + )} + {themeOption === "dark" && ( + + )} + {themeOption === "system" && ( + + )} + + {themeOption} + + {userTheme === themeOption && ( + + + + )} + + ) + )} + + logout()}> + + Log out + +
+
+ + {user.email} + +
+ + ); +} diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index ccf56da4..cadb7f7e 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -50,7 +50,7 @@ const CardDescription = React.forwardRef< >(({ className, ...props }, ref) => (

));