starting to come together (:
Some checks failed
Deploy / deploy (ubuntu-latest, 2.44.0) (push) Failing after 1m17s
Some checks failed
Deploy / deploy (ubuntu-latest, 2.44.0) (push) Failing after 1m17s
This commit is contained in:
parent
c8eae3330a
commit
30f5e779de
@ -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
|
@ -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>
|
||||||
|
84
src/app/provider/organization-provider.tsx
Normal file
84
src/app/provider/organization-provider.tsx
Normal file
@ -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";
|
||||||
|
71
src/app/store/organization-store.ts
Normal file
71
src/app/store/organization-store.ts
Normal file
@ -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 = {
|
||||||
/**
|
/**
|
37
src/app/types/org/organization.ts
Normal file
37
src/app/types/org/organization.ts
Normal file
@ -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[];
|
||||||
|
};
|
50
src/app/types/page/status-page.ts
Normal file
50
src/app/types/page/status-page.ts
Normal file
@ -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;
|
||||||
|
};
|
21
src/app/types/sidebar-link.ts
Normal file
21
src/app/types/sidebar-link.ts
Normal file
@ -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";
|
||||||
|
82
src/components/dashboard/sidebar/links.tsx
Normal file
82
src/components/dashboard/sidebar/links.tsx
Normal file
@ -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;
|
133
src/components/dashboard/sidebar/organization-selector.tsx
Normal file
133
src/components/dashboard/sidebar/organization-selector.tsx
Normal file
@ -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;
|
26
src/components/dashboard/sidebar/sidebar.tsx
Normal file
26
src/components/dashboard/sidebar/sidebar.tsx
Normal file
@ -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;
|
47
src/components/simple-tooltip.tsx
Normal file
47
src/components/simple-tooltip.tsx
Normal file
@ -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;
|
158
src/components/ui/command.tsx
Normal file
158
src/components/ui/command.tsx
Normal file
@ -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,
|
||||||
|
};
|
122
src/components/ui/dialog.tsx
Normal file
122
src/components/ui/dialog.tsx
Normal file
@ -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,
|
||||||
|
};
|
33
src/components/ui/popover.tsx
Normal file
33
src/components/ui/popover.tsx
Normal file
@ -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 };
|
30
src/components/ui/tooltip.tsx
Normal file
30
src/components/ui/tooltip.tsx
Normal file
@ -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 };
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user