starting to come together (:
Some checks failed
Deploy / deploy (ubuntu-latest, 2.44.0) (push) Failing after 1m17s

This commit is contained in:
Braydon 2024-09-18 23:32:07 -04:00
parent c8eae3330a
commit 30f5e779de
28 changed files with 969 additions and 25 deletions

@ -1 +1,3 @@
NEXT_PUBLIC_API_ENDPOINT=https://api.pulseapp.cc/v1 NEXT_PUBLIC_API_ENDPOINT=http://localhost:7500/v1
NEXT_PUBLIC_CDN_ENDPOINT=https://cdn.pulseapp.cc
NEXT_PUBLIC_CAPTCHA_SITE_KEY=CHANGE_ME

BIN
bun.lockb

Binary file not shown.

@ -1,6 +1,14 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
output: "standalone", output: "standalone",
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.pulseapp.cc",
},
],
},
}; };
export default nextConfig; export default nextConfig;

@ -37,6 +37,7 @@
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.53.0", "react-hook-form": "^7.53.0",
"react-initials-avatar": "^1.1.2",
"react-turnstile": "^1.1.3", "react-turnstile": "^1.1.3",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"sonner": "^1.5.0", "sonner": "^1.5.0",

@ -1,5 +1,7 @@
import { ReactElement, ReactNode } from "react"; import { ReactElement, ReactNode } from "react";
import UserProvider from "@/app/provider/user-provider"; import UserProvider from "@/app/provider/user-provider";
import Sidebar from "@/components/dashboard/sidebar/sidebar";
import OrganizationProvider from "@/app/provider/organization-provider";
/** /**
* The layout for the dashboard pages. * The layout for the dashboard pages.
@ -11,5 +13,14 @@ const DashboardLayout = ({
children, children,
}: Readonly<{ }: Readonly<{
children: ReactNode; children: ReactNode;
}>): ReactElement => <UserProvider>{children}</UserProvider>; }>): ReactElement => (
<UserProvider>
<OrganizationProvider>
<div className="min-h-screen flex">
<Sidebar />
{children}
</div>
</OrganizationProvider>
</UserProvider>
);
export default DashboardLayout; export default DashboardLayout;

@ -3,7 +3,7 @@
import { ReactElement } from "react"; import { ReactElement } from "react";
import OnboardingForm from "@/components/dashboard/onboarding/onboarding-form"; import OnboardingForm from "@/components/dashboard/onboarding/onboarding-form";
import { useUserContext } from "@/app/provider/user-provider"; import { useUserContext } from "@/app/provider/user-provider";
import { UserState } from "@/app/store/user-store-props"; import { UserState } from "@/app/store/user-store";
import { User } from "@/app/types/user/user"; import { User } from "@/app/types/user/user";
import { hasFlag } from "@/lib/user"; import { hasFlag } from "@/lib/user";
import { UserFlag } from "@/app/types/user/user-flag"; import { UserFlag } from "@/app/types/user/user-flag";

@ -1,17 +1,23 @@
"use client"; "use client";
import { ReactElement } from "react"; import { ReactElement } from "react";
import { UserState } from "@/app/store/user-store-props"; import { UserState } from "@/app/store/user-store";
import { User } from "@/app/types/user/user"; import { User } from "@/app/types/user/user";
import { useUserContext } from "@/app/provider/user-provider"; import { useUserContext } from "@/app/provider/user-provider";
import { useOrganizationContext } from "@/app/provider/organization-provider";
import { OrganizationState } from "@/app/store/organization-store";
const DashboardPage = (): ReactElement => { const DashboardPage = (): ReactElement => {
const user: User | undefined = useUserContext( const user: User | undefined = useUserContext(
(state: UserState) => state.user (state: UserState) => state.user
); );
const selectedOrganization: bigint | undefined = useOrganizationContext(
(state: OrganizationState) => state.selected
);
return ( return (
<main className="min-h-screen"> <main>
PulseApp Dashboard, hello {user?.email} PulseApp Dashboard, hello {user?.email}, selected org:{" "}
{selectedOrganization}
</main> </main>
); );
}; };

@ -7,6 +7,7 @@ import { NextFont } from "next/dist/compiled/@next/font";
import { ThemeProvider } from "@/components/theme-provider"; import { ThemeProvider } from "@/components/theme-provider";
import { CookiesProvider } from "next-client-cookies/server"; import { CookiesProvider } from "next-client-cookies/server";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
const inter: NextFont = Inter({ subsets: ["latin"] }); const inter: NextFont = Inter({ subsets: ["latin"] });
@ -55,7 +56,9 @@ const RootLayout = ({
}} }}
> >
<CookiesProvider> <CookiesProvider>
{children} <TooltipProvider delayDuration={100}>
{children}
</TooltipProvider>
<Toaster /> <Toaster />
</CookiesProvider> </CookiesProvider>
</div> </div>

@ -0,0 +1,84 @@
"use client";
import { ReactNode, useCallback, useContext, useEffect, useRef } from "react";
import { UserState } from "@/app/store/user-store";
import { User } from "@/app/types/user/user";
import { StoreApi, useStore } from "zustand";
import createOrganizationStore, {
OrganizationContext,
OrganizationState,
OrganizationStore,
} from "@/app/store/organization-store";
import { apiRequest } from "@/lib/api";
import { Organization } from "@/app/types/org/organization";
import { useUserContext } from "@/app/provider/user-provider";
import { Session } from "@/app/types/user/session";
/**
* The provider that will provide organization context to children.
*
* @param children the children to provide context to
* @return the provider
*/
const OrganizationProvider = ({ children }: { children: ReactNode }) => {
const session: Session | undefined = useUserContext(
(state: UserState) => state.session
);
const user: User | undefined = useUserContext(
(state: UserState) => state.user
);
const storeRef = useRef<OrganizationStore>();
if (!storeRef.current) {
storeRef.current = createOrganizationStore();
}
/**
* Fetch the organizations for the logged in user.
*/
const fetchOrganizations = useCallback(async () => {
let selectedOrganization: string | null = localStorage.getItem(
"selected-organization"
);
const { data, error } = await apiRequest<Organization[]>({
endpoint: "/organization/@me",
session,
});
const organizations: Organization[] = data as Organization[];
if (!selectedOrganization && organizations.length > 0) {
selectedOrganization = organizations[0].snowflake;
}
storeRef.current
?.getState()
.update(selectedOrganization || undefined, organizations);
}, [session]);
useEffect(() => {
fetchOrganizations();
}, [fetchOrganizations]);
return (
<OrganizationContext.Provider value={storeRef.current}>
{children}
</OrganizationContext.Provider>
);
};
/**
* Use the organization context.
*
* @param selector the state selector to use
* @return the value returned by the selector
*/
export function useOrganizationContext<T>(
selector: (state: OrganizationState) => T
): T {
const store: StoreApi<OrganizationState> | null =
useContext(OrganizationContext);
if (!store) {
throw new Error("Missing OrganizationContext.Provider in the tree");
}
return useStore(store, selector);
}
export default OrganizationProvider;

@ -12,7 +12,7 @@ import createUserStore, {
UserContext, UserContext,
UserState, UserState,
UserStore, UserStore,
} from "@/app/store/user-store-props"; } from "@/app/store/user-store";
import { User } from "@/app/types/user/user"; import { User } from "@/app/types/user/user";
import { Cookies, useCookies } from "next-client-cookies"; import { Cookies, useCookies } from "next-client-cookies";
import { Session } from "@/app/types/user/session"; import { Session } from "@/app/types/user/session";

@ -0,0 +1,71 @@
import { createStore } from "zustand";
import { createContext } from "react";
import { Organization } from "@/app/types/org/organization";
/**
* The context to provide this store.
*/
export const OrganizationContext = createContext<OrganizationStore | null>(
null
);
/**
* The props in this store.
*/
export type OrganizationStoreProps = {
/**
* The currently selected organization.
*/
selected: string | undefined;
/**
* The organization's the user has.
*/
organizations: Organization[];
};
/**
* The organization store state.
*/
export type OrganizationState = OrganizationStoreProps & {
/**
* Update the state.
*
* @param selected the selected organization
* @param organizations the user's organizations
*/
update: (
selected: string | undefined,
organizations: Organization[]
) => void;
/**
* Set the selected organization.
*
* @param selected the selected organization
*/
setSelected: (selected: string | undefined) => void;
};
/**
* The type representing the organization store.
*/
export type OrganizationStore = ReturnType<typeof createOrganizationStore>;
/**
* Create a new user store.
*/
const createOrganizationStore = () => {
const defaultProps: OrganizationStoreProps = {
selected: undefined,
organizations: [],
};
return createStore<OrganizationState>()((set) => ({
...defaultProps,
update: (selected: string | undefined, organizations: Organization[]) =>
set(() => ({ selected, organizations })),
setSelected: (selected: string | undefined) =>
set(() => ({ selected })),
}));
};
export default createOrganizationStore;

@ -3,10 +3,13 @@ import { User } from "@/app/types/user/user";
import { createContext } from "react"; import { createContext } from "react";
import { Session } from "@/app/types/user/session"; import { Session } from "@/app/types/user/session";
/**
* The context to provide this store.
*/
export const UserContext = createContext<UserStore | null>(null); export const UserContext = createContext<UserStore | null>(null);
/** /**
* The props in the store. * The props in this store.
*/ */
export type UserStoreProps = { export type UserStoreProps = {
/** /**

@ -0,0 +1,37 @@
import { StatusPage } from "@/app/types/page/status-page";
/**
* An organization owned by a {@link User}.
*/
export type Organization = {
/**
* The snowflake id of this organization.
*/
snowflake: string;
/**
* The name of this organization.
*/
name: string;
/**
* The slug of this organization.
*/
slug: string;
/**
* The hash to the logo of this organization, if any.
*/
logo: string;
/**
* The snowflake of the {@link User}
* that owns this organization.
*/
ownerSnowflake: number;
/**
* The status pages owned by this organization.
*/
statusPages: StatusPage[];
};

@ -0,0 +1,50 @@
/**
* A status page owned by an {@link Organization}.
*/
export type StatusPage = {
/**
* The snowflake id of this status page.
*/
snowflake: bigint;
/**
* The name of this status page.
*/
name: string;
/**
* The slug of this status page.
*/
slug: string;
/**
* The description of this status page, if any.
*/
description: string | undefined;
/**
* The hash to the logo of this status page, if any.
*/
logo: string | undefined;
/**
* The hash to the banner of this status page, if any.
*/
banner: string | undefined;
/**
* The theme of this status page.
*/
theme: "AUTO" | "DARK" | "LIGHT";
/**
* Whether this status page is visible in search engines.
*/
visibleInSearchEngines: boolean;
/**
* The snowflake of the {@link Organization}
* that owns this status page.
*/
orgSnowflake: boolean;
};

@ -0,0 +1,21 @@
import { ReactElement } from "react";
/**
* A link on the dashboard sidebar.
*/
export type SidebarLink = {
/**
* The name of this link.
*/
name: string;
/**
* The icon for this link.
*/
icon: ReactElement;
/**
* The href for this link.
*/
href: string;
};

@ -15,5 +15,5 @@ export type Session = {
/** /**
* The unix time this session expires. * The unix time this session expires.
*/ */
expires: number; expires: bigint;
}; };

@ -2,7 +2,7 @@ export type User = {
/** /**
* The snowflake id of this user. * The snowflake id of this user.
*/ */
snowflake: `${bigint}`; snowflake: bigint;
/** /**
* This user's email. * This user's email.

@ -4,10 +4,11 @@ import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const brandingVariants = cva( const brandingVariants = cva(
"relative hover:opacity-75 select-none transition-all transform-gpu", "relative group-hover:opacity-75 hover:opacity-75 select-none transition-all transform-gpu",
{ {
variants: { variants: {
size: { size: {
xs: "w-10 h-10",
sm: "w-16 h-16", sm: "w-16 h-16",
default: "w-24 h-24", default: "w-24 h-24",
lg: "w-32 h-32", lg: "w-32 h-32",
@ -23,10 +24,15 @@ const brandingVariants = cva(
* The props for this component. * The props for this component.
*/ */
type BrandingProps = { type BrandingProps = {
/**
* The href to go to when clicked.
*/
href?: string;
/** /**
* The size of the branding. * The size of the branding.
*/ */
size?: "sm" | "default" | "lg"; size?: "xs" | "sm" | "default" | "lg";
/** /**
* The optional class name to apply to the branding. * The optional class name to apply to the branding.
@ -34,8 +40,11 @@ type BrandingProps = {
className?: string; className?: string;
}; };
const Branding = ({ size, className }: BrandingProps) => ( const Branding = ({ href, size, className }: BrandingProps) => (
<Link className={cn(brandingVariants({ size, className }))} href="/"> <Link
className={cn(brandingVariants({ size, className }))}
href={href ?? "/"}
>
<Image src="/media/logo.png" alt="PulseApp Logo" fill /> <Image src="/media/logo.png" alt="PulseApp Logo" fill />
</Link> </Link>
); );

@ -14,7 +14,7 @@ import { z } from "zod";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { apiRequest } from "@/lib/api"; import { apiRequest } from "@/lib/api";
import { useUserContext } from "@/app/provider/user-provider"; import { useUserContext } from "@/app/provider/user-provider";
import { UserState } from "@/app/store/user-store-props"; import { UserState } from "@/app/store/user-store";
import { Session } from "@/app/types/user/session"; import { Session } from "@/app/types/user/session";
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime"; import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";

@ -0,0 +1,82 @@
"use client";
import { ReactElement } from "react";
import { SidebarLink } from "@/app/types/sidebar-link";
import SimpleTooltip from "@/components/simple-tooltip";
import Link from "next/link";
import { cn } from "@/lib/utils";
import {
ChartBarSquareIcon,
ClipboardIcon,
Cog6ToothIcon,
FireIcon,
WrenchIcon,
} from "@heroicons/react/24/outline";
import { useOrganizationContext } from "@/app/provider/organization-provider";
import { OrganizationState } from "@/app/store/organization-store";
const links: SidebarLink[] = [
{
name: "Status Pages",
icon: <ClipboardIcon />,
href: "/status-pages",
},
{
name: "Automations",
icon: <WrenchIcon />,
href: "/automations",
},
{
name: "Incidents",
icon: <FireIcon />,
href: "/incidents",
},
{
name: "Insights",
icon: <ChartBarSquareIcon />,
href: "/insights",
},
{
name: "Settings",
icon: <Cog6ToothIcon />,
href: "/settings",
},
];
/**
* The links to display on
* the dashboard sidebar.
*
* @return the links jsx
*/
const Links = (): ReactElement => {
const selectedOrganization: string | undefined = useOrganizationContext(
(state: OrganizationState) => state.selected
);
return (
<div className="mt-3.5 w-full flex flex-col gap-0.5">
{links.map((link: SidebarLink, index: number) => {
const active: boolean = index === 0;
return (
<SimpleTooltip
key={index}
content={`Visit ${link.name}`}
side="right"
>
<Link
className={cn(
"px-3 py-2 flex gap-2 items-center text-sm rounded-lg hover:bg-zinc-800 transition-all transform-gpu",
active && "font-medium bg-zinc-800"
)}
href={`/dashboard/org/${selectedOrganization}${link.href}`}
>
<div className="relative w-5 h-5">{link.icon}</div>
{link.name}
</Link>
</SimpleTooltip>
);
})}
</div>
);
};
export default Links;

@ -0,0 +1,133 @@
"use client";
import * as React from "react";
import { ReactElement, useEffect, useState } from "react";
import { ChevronsUpDownIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import InitialsAvatar from "react-initials-avatar";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useOrganizationContext } from "@/app/provider/organization-provider";
import { OrganizationState } from "@/app/store/organization-store";
import { Organization } from "@/app/types/org/organization";
import { CheckIcon } from "@heroicons/react/24/outline";
import Image from "next/image";
/**
* The organization selector.
*
* @return the selector jsx
*/
const OrganizationSelector = (): ReactElement => {
const selectedOrganization: string | undefined = useOrganizationContext(
(state: OrganizationState) => state.selected
);
const setSelectedOrganization = useOrganizationContext(
(state) => state.setSelected
);
const organizations: Organization[] = useOrganizationContext(
(state: OrganizationState) => state.organizations
);
const [open, setOpen] = useState<boolean>(false);
const [selected, setSelected] = useState<Organization | undefined>();
// Set the selected organization
useEffect(() => {
setSelected(
organizations.find((organization: Organization) => {
return organization.snowflake === selectedOrganization;
})
);
}, [organizations, selectedOrganization]);
/**
* Handle selecting an organization.
*
* @param organization the selected organization
*/
const selectOrganization = (organization: Organization) => {
setOpen(false);
setSelected(organization);
setSelectedOrganization(organization.snowflake);
localStorage.setItem("selected-organization", organization.snowflake);
};
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
className="w-52 bg-background/30 justify-between"
aria-expanded={open}
variant="outline"
role="combobox"
>
{selected ? (
<div className="flex gap-2.5 items-center">
<div className="relative p-0.5 w-5 h-5 rounded-full">
{selected.logo ? (
<Image
className="rounded-full"
src={`${process.env.NEXT_PUBLIC_CDN_ENDPOINT}/organizations/${selected.logo}.webp`}
alt={`${selected.name}'s Logo`}
fill
/>
) : (
<InitialsAvatar name={selected.name} />
)}
</div>
{selected.name}
</div>
) : (
"Select organization..."
)}
<ChevronsUpDownIcon className="ml-2 w-4 h-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-52">
<Command>
<CommandInput placeholder="Search organization..." />
<CommandList>
<CommandEmpty>No organizations found.</CommandEmpty>
<CommandGroup>
{organizations.map(
(organization: Organization, index: number) => (
<CommandItem
key={index}
value={organization.name}
onSelect={(currentValue: string) =>
selectOrganization(
organizations.find(
(organization) =>
organization.name ===
currentValue
) as Organization
)
}
>
{selected?.snowflake ===
selectedOrganization && (
<CheckIcon className="mr-2 w-4 h-4" />
)}
{organization.name}
</CommandItem>
)
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
export default OrganizationSelector;

@ -0,0 +1,26 @@
import { ReactElement } from "react";
import Branding from "@/components/branding";
import { Separator } from "@/components/ui/separator";
import Link from "next/link";
import OrganizationSelector from "@/components/dashboard/sidebar/organization-selector";
import Links from "@/components/dashboard/sidebar/links";
const Sidebar = (): ReactElement => (
<nav className="w-56 px-3 py-4 h-screen flex flex-col items-center bg-zinc-900 border-r">
{/* Header */}
<Link className="flex gap-3 items-center group" href="/dashboard">
<Branding size="xs" />
<h1 className="text-xl font-bold group-hover:opacity-75 transition-all transform-gpu">
Pulse App
</h1>
</Link>
<Separator className="w-32 my-3.5" />
{/* Content */}
<OrganizationSelector />
<Links />
<div className="mt-auto">USER</div>
</nav>
);
export default Sidebar;

@ -0,0 +1,47 @@
import { ReactElement, ReactNode } from "react";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { SIDE_OPTIONS } from "@radix-ui/react-popper";
/**
* The props for a simple tooltip.
*/
type SimpleTooltipProps = {
/**
* The content to display in the tooltip.
*/
content: string | ReactElement;
/**
* The side to display the tooltip on.
*/
side?: (typeof SIDE_OPTIONS)[number];
/**
* The children to render in this tooltip.
*/
children: ReactNode;
};
/**
* A simple tooltip, this is wrapping the
* shadcn tooltip to make it easier to use.
*
* @return the tooltip jsx
*/
const SimpleTooltip = ({
content,
side,
children,
}: SimpleTooltipProps): ReactElement => (
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent className="bg-muted text-white" side={side}>
{content}
</TooltipContent>
</Tooltip>
);
export default SimpleTooltip;

@ -0,0 +1,158 @@
"use client";
import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
import { Command as CommandPrimitive } from "cmdk";
import { cn } from "@/lib/utils";
import { Dialog, DialogContent } from "@/components/ui/dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<MagnifyingGlassIcon className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn(
"max-h-[300px] overflow-y-auto overflow-x-hidden",
className
)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

@ -0,0 +1,122 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

@ -0,0 +1,33 @@
"use client";
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

@ -0,0 +1,30 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-105 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

@ -42,6 +42,18 @@ export const apiRequest = async <T>({
data: T | undefined; data: T | undefined;
error: ApiError | undefined; error: ApiError | undefined;
}> => { }> => {
// Build the request headers
let headers: HeadersInit = {
"Content-Type": `application/${method === "POST" ? "x-www-form-urlencoded" : "json"}`,
};
if (session) {
headers = {
...headers,
Authorization: `Bearer ${session.accessToken}`,
};
}
// Send the request
const response: Response = await fetch( const response: Response = await fetch(
`${process.env.NEXT_PUBLIC_API_ENDPOINT}${endpoint}`, `${process.env.NEXT_PUBLIC_API_ENDPOINT}${endpoint}`,
{ {
@ -50,17 +62,12 @@ export const apiRequest = async <T>({
method === "POST" && body method === "POST" && body
? new URLSearchParams(body) ? new URLSearchParams(body)
: undefined, : undefined,
headers: { headers,
"Content-Type": `application/${method === "POST" ? "x-www-form-urlencoded" : "json"}`,
...(session
? {
Authorization: `Bearer ${session.accessToken}`,
}
: {}),
},
} }
); );
const json: any = parseJson(await response.text()); // Parse the Json response from the API
// Parse the Json response from the API
const json: any = parseJson(await response.text());
if (response.status !== 200) { if (response.status !== 200) {
return { data: undefined, error: json as ApiError }; return { data: undefined, error: json as ApiError };
} }