From 30f5e779dee397b1fa0914a45aa2ce33812bfa12 Mon Sep 17 00:00:00 2001 From: Rainnny7 Date: Wed, 18 Sep 2024 23:32:07 -0400 Subject: [PATCH] starting to come together (: --- .env.example | 4 +- bun.lockb | Bin 197035 -> 197482 bytes next.config.mjs | 8 + package.json | 1 + src/app/(pages)/dashboard/layout.tsx | 13 +- src/app/(pages)/dashboard/onboarding/page.tsx | 2 +- src/app/(pages)/dashboard/page.tsx | 12 +- src/app/(pages)/layout.tsx | 5 +- src/app/provider/organization-provider.tsx | 84 ++++++++++ src/app/provider/user-provider.tsx | 2 +- src/app/store/organization-store.ts | 71 ++++++++ .../{user-store-props.ts => user-store.ts} | 5 +- src/app/types/org/organization.ts | 37 ++++ src/app/types/page/status-page.ts | 50 ++++++ src/app/types/sidebar-link.ts | 21 +++ src/app/types/user/session.ts | 2 +- src/app/types/user/user.ts | 2 +- src/components/branding.tsx | 17 +- .../dashboard/onboarding/onboarding-form.tsx | 2 +- src/components/dashboard/sidebar/links.tsx | 82 +++++++++ .../sidebar/organization-selector.tsx | 133 +++++++++++++++ src/components/dashboard/sidebar/sidebar.tsx | 26 +++ src/components/simple-tooltip.tsx | 47 ++++++ src/components/ui/command.tsx | 158 ++++++++++++++++++ src/components/ui/dialog.tsx | 122 ++++++++++++++ src/components/ui/popover.tsx | 33 ++++ src/components/ui/tooltip.tsx | 30 ++++ src/lib/api.ts | 25 ++- 28 files changed, 969 insertions(+), 25 deletions(-) create mode 100644 src/app/provider/organization-provider.tsx create mode 100644 src/app/store/organization-store.ts rename src/app/store/{user-store-props.ts => user-store.ts} (94%) create mode 100644 src/app/types/org/organization.ts create mode 100644 src/app/types/page/status-page.ts create mode 100644 src/app/types/sidebar-link.ts create mode 100644 src/components/dashboard/sidebar/links.tsx create mode 100644 src/components/dashboard/sidebar/organization-selector.tsx create mode 100644 src/components/dashboard/sidebar/sidebar.tsx create mode 100644 src/components/simple-tooltip.tsx create mode 100644 src/components/ui/command.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/tooltip.tsx diff --git a/.env.example b/.env.example index 22ab8ed..9a4da8b 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,3 @@ -NEXT_PUBLIC_API_ENDPOINT=https://api.pulseapp.cc/v1 \ No newline at end of file +NEXT_PUBLIC_API_ENDPOINT=http://localhost:7500/v1 +NEXT_PUBLIC_CDN_ENDPOINT=https://cdn.pulseapp.cc +NEXT_PUBLIC_CAPTCHA_SITE_KEY=CHANGE_ME \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 2574444cc801531092ce7c0eae7bd9ff9b9cb131..d6784b2fe7b8ed5212d3a4bd77a2954bfa2ce271 100644 GIT binary patch delta 34460 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 diff --git a/next.config.mjs b/next.config.mjs index 608f1be..ffc3dcb 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,14 @@ /** @type {import('next').NextConfig} */ const nextConfig = { output: "standalone", + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "cdn.pulseapp.cc", + }, + ], + }, }; export default nextConfig; diff --git a/package.json b/package.json index ec66efd..86ee8d7 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.53.0", + "react-initials-avatar": "^1.1.2", "react-turnstile": "^1.1.3", "sharp": "^0.33.5", "sonner": "^1.5.0", diff --git a/src/app/(pages)/dashboard/layout.tsx b/src/app/(pages)/dashboard/layout.tsx index 2892eef..330bc52 100644 --- a/src/app/(pages)/dashboard/layout.tsx +++ b/src/app/(pages)/dashboard/layout.tsx @@ -1,5 +1,7 @@ import { ReactElement, ReactNode } from "react"; 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. @@ -11,5 +13,14 @@ const DashboardLayout = ({ children, }: Readonly<{ children: ReactNode; -}>): ReactElement => {children}; +}>): ReactElement => ( + + +
+ + {children} +
+
+
+); export default DashboardLayout; diff --git a/src/app/(pages)/dashboard/onboarding/page.tsx b/src/app/(pages)/dashboard/onboarding/page.tsx index b0dcdbf..51aa9a2 100644 --- a/src/app/(pages)/dashboard/onboarding/page.tsx +++ b/src/app/(pages)/dashboard/onboarding/page.tsx @@ -3,7 +3,7 @@ import { ReactElement } from "react"; import OnboardingForm from "@/components/dashboard/onboarding/onboarding-form"; 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 { hasFlag } from "@/lib/user"; import { UserFlag } from "@/app/types/user/user-flag"; diff --git a/src/app/(pages)/dashboard/page.tsx b/src/app/(pages)/dashboard/page.tsx index 4f203f6..dedd503 100644 --- a/src/app/(pages)/dashboard/page.tsx +++ b/src/app/(pages)/dashboard/page.tsx @@ -1,17 +1,23 @@ "use client"; 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 { useUserContext } from "@/app/provider/user-provider"; +import { useOrganizationContext } from "@/app/provider/organization-provider"; +import { OrganizationState } from "@/app/store/organization-store"; const DashboardPage = (): ReactElement => { const user: User | undefined = useUserContext( (state: UserState) => state.user ); + const selectedOrganization: bigint | undefined = useOrganizationContext( + (state: OrganizationState) => state.selected + ); return ( -
- PulseApp Dashboard, hello {user?.email} +
+ PulseApp Dashboard, hello {user?.email}, selected org:{" "} + {selectedOrganization}
); }; diff --git a/src/app/(pages)/layout.tsx b/src/app/(pages)/layout.tsx index f550700..4faaf8f 100644 --- a/src/app/(pages)/layout.tsx +++ b/src/app/(pages)/layout.tsx @@ -7,6 +7,7 @@ import { NextFont } from "next/dist/compiled/@next/font"; import { ThemeProvider } from "@/components/theme-provider"; import { CookiesProvider } from "next-client-cookies/server"; import { Toaster } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; const inter: NextFont = Inter({ subsets: ["latin"] }); @@ -55,7 +56,9 @@ const RootLayout = ({ }} > - {children} + + {children} + diff --git a/src/app/provider/organization-provider.tsx b/src/app/provider/organization-provider.tsx new file mode 100644 index 0000000..c6d5ea9 --- /dev/null +++ b/src/app/provider/organization-provider.tsx @@ -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(); + 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({ + 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 ( + + {children} + + ); +}; + +/** + * Use the organization context. + * + * @param selector the state selector to use + * @return the value returned by the selector + */ +export function useOrganizationContext( + selector: (state: OrganizationState) => T +): T { + const store: StoreApi | null = + useContext(OrganizationContext); + if (!store) { + throw new Error("Missing OrganizationContext.Provider in the tree"); + } + return useStore(store, selector); +} + +export default OrganizationProvider; diff --git a/src/app/provider/user-provider.tsx b/src/app/provider/user-provider.tsx index d6c75f5..6bdd4e5 100644 --- a/src/app/provider/user-provider.tsx +++ b/src/app/provider/user-provider.tsx @@ -12,7 +12,7 @@ import createUserStore, { UserContext, UserState, UserStore, -} from "@/app/store/user-store-props"; +} from "@/app/store/user-store"; import { User } from "@/app/types/user/user"; import { Cookies, useCookies } from "next-client-cookies"; import { Session } from "@/app/types/user/session"; diff --git a/src/app/store/organization-store.ts b/src/app/store/organization-store.ts new file mode 100644 index 0000000..3443c13 --- /dev/null +++ b/src/app/store/organization-store.ts @@ -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( + 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; + +/** + * Create a new user store. + */ +const createOrganizationStore = () => { + const defaultProps: OrganizationStoreProps = { + selected: undefined, + organizations: [], + }; + return createStore()((set) => ({ + ...defaultProps, + update: (selected: string | undefined, organizations: Organization[]) => + set(() => ({ selected, organizations })), + setSelected: (selected: string | undefined) => + set(() => ({ selected })), + })); +}; +export default createOrganizationStore; diff --git a/src/app/store/user-store-props.ts b/src/app/store/user-store.ts similarity index 94% rename from src/app/store/user-store-props.ts rename to src/app/store/user-store.ts index 08d284f..d572534 100644 --- a/src/app/store/user-store-props.ts +++ b/src/app/store/user-store.ts @@ -3,10 +3,13 @@ import { User } from "@/app/types/user/user"; import { createContext } from "react"; import { Session } from "@/app/types/user/session"; +/** + * The context to provide this store. + */ export const UserContext = createContext(null); /** - * The props in the store. + * The props in this store. */ export type UserStoreProps = { /** diff --git a/src/app/types/org/organization.ts b/src/app/types/org/organization.ts new file mode 100644 index 0000000..d13b923 --- /dev/null +++ b/src/app/types/org/organization.ts @@ -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[]; +}; diff --git a/src/app/types/page/status-page.ts b/src/app/types/page/status-page.ts new file mode 100644 index 0000000..5f07811 --- /dev/null +++ b/src/app/types/page/status-page.ts @@ -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; +}; diff --git a/src/app/types/sidebar-link.ts b/src/app/types/sidebar-link.ts new file mode 100644 index 0000000..1635be8 --- /dev/null +++ b/src/app/types/sidebar-link.ts @@ -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; +}; diff --git a/src/app/types/user/session.ts b/src/app/types/user/session.ts index 958b82d..8beb7fa 100644 --- a/src/app/types/user/session.ts +++ b/src/app/types/user/session.ts @@ -15,5 +15,5 @@ export type Session = { /** * The unix time this session expires. */ - expires: number; + expires: bigint; }; diff --git a/src/app/types/user/user.ts b/src/app/types/user/user.ts index a9d9a2d..301162b 100644 --- a/src/app/types/user/user.ts +++ b/src/app/types/user/user.ts @@ -2,7 +2,7 @@ export type User = { /** * The snowflake id of this user. */ - snowflake: `${bigint}`; + snowflake: bigint; /** * This user's email. diff --git a/src/components/branding.tsx b/src/components/branding.tsx index 7e02e68..7e777dc 100644 --- a/src/components/branding.tsx +++ b/src/components/branding.tsx @@ -4,10 +4,11 @@ import { cva } from "class-variance-authority"; import { cn } from "@/lib/utils"; 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: { size: { + xs: "w-10 h-10", sm: "w-16 h-16", default: "w-24 h-24", lg: "w-32 h-32", @@ -23,10 +24,15 @@ const brandingVariants = cva( * The props for this component. */ type BrandingProps = { + /** + * The href to go to when clicked. + */ + href?: string; + /** * The size of the branding. */ - size?: "sm" | "default" | "lg"; + size?: "xs" | "sm" | "default" | "lg"; /** * The optional class name to apply to the branding. @@ -34,8 +40,11 @@ type BrandingProps = { className?: string; }; -const Branding = ({ size, className }: BrandingProps) => ( - +const Branding = ({ href, size, className }: BrandingProps) => ( + PulseApp Logo ); diff --git a/src/components/dashboard/onboarding/onboarding-form.tsx b/src/components/dashboard/onboarding/onboarding-form.tsx index 3d18ba9..d11def9 100644 --- a/src/components/dashboard/onboarding/onboarding-form.tsx +++ b/src/components/dashboard/onboarding/onboarding-form.tsx @@ -14,7 +14,7 @@ import { z } from "zod"; import { motion } from "framer-motion"; import { apiRequest } from "@/lib/api"; 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 { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime"; import { useRouter } from "next/navigation"; diff --git a/src/components/dashboard/sidebar/links.tsx b/src/components/dashboard/sidebar/links.tsx new file mode 100644 index 0000000..e1c62f3 --- /dev/null +++ b/src/components/dashboard/sidebar/links.tsx @@ -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: , + href: "/status-pages", + }, + { + name: "Automations", + icon: , + href: "/automations", + }, + { + name: "Incidents", + icon: , + href: "/incidents", + }, + { + name: "Insights", + icon: , + href: "/insights", + }, + { + name: "Settings", + icon: , + 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 ( +
+ {links.map((link: SidebarLink, index: number) => { + const active: boolean = index === 0; + return ( + + +
{link.icon}
+ {link.name} + +
+ ); + })} +
+ ); +}; +export default Links; diff --git a/src/components/dashboard/sidebar/organization-selector.tsx b/src/components/dashboard/sidebar/organization-selector.tsx new file mode 100644 index 0000000..adf7fc9 --- /dev/null +++ b/src/components/dashboard/sidebar/organization-selector.tsx @@ -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(false); + const [selected, setSelected] = useState(); + + // 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 ( + + + + + + + + + No organizations found. + + {organizations.map( + (organization: Organization, index: number) => ( + + selectOrganization( + organizations.find( + (organization) => + organization.name === + currentValue + ) as Organization + ) + } + > + {selected?.snowflake === + selectedOrganization && ( + + )} + {organization.name} + + ) + )} + + + + + + ); +}; +export default OrganizationSelector; diff --git a/src/components/dashboard/sidebar/sidebar.tsx b/src/components/dashboard/sidebar/sidebar.tsx new file mode 100644 index 0000000..9a40409 --- /dev/null +++ b/src/components/dashboard/sidebar/sidebar.tsx @@ -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 => ( + +); + +export default Sidebar; diff --git a/src/components/simple-tooltip.tsx b/src/components/simple-tooltip.tsx new file mode 100644 index 0000000..1f33a10 --- /dev/null +++ b/src/components/simple-tooltip.tsx @@ -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 => ( + + {children} + + {content} + + +); +export default SimpleTooltip; diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..dda4c34 --- /dev/null +++ b/src/components/ui/command.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +CommandShortcut.displayName = "CommandShortcut"; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..a33c04d --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..1c1d1c5 --- /dev/null +++ b/src/components/ui/popover.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..9427e9c --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/src/lib/api.ts b/src/lib/api.ts index 0102bf2..a811df8 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -42,6 +42,18 @@ export const apiRequest = async ({ data: T | 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( `${process.env.NEXT_PUBLIC_API_ENDPOINT}${endpoint}`, { @@ -50,17 +62,12 @@ export const apiRequest = async ({ method === "POST" && body ? new URLSearchParams(body) : undefined, - headers: { - "Content-Type": `application/${method === "POST" ? "x-www-form-urlencoded" : "json"}`, - ...(session - ? { - Authorization: `Bearer ${session.accessToken}`, - } - : {}), - }, + headers, } ); - 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) { return { data: undefined, error: json as ApiError }; }