diff --git a/apps/console/package.json b/apps/console/package.json index 073856c4b..f2b97729a 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -10,6 +10,13 @@ "lint": "eslint src --ext .ts,.tsx" }, "dependencies": { + "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-collapsible": "^1.1.3", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tooltip": "^1.1.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.475.0", diff --git a/apps/console/src/App.css b/apps/console/src/App.css index a1be0c343..5e7f825c8 100644 --- a/apps/console/src/App.css +++ b/apps/console/src/App.css @@ -24,6 +24,14 @@ --destructive-foreground: 210 40% 98%; --ring: 215 20.2% 65.1%; --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; } .dark { @@ -46,6 +54,14 @@ --destructive: 0 63% 31%; --destructive-foreground: 210 40% 98%; --ring: 216 34% 17%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; } } @@ -57,3 +73,12 @@ @apply font-sans antialiased bg-background text-foreground; } } + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/console/src/App.tsx b/apps/console/src/App.tsx index 5849dbbab..9eefb3da3 100644 --- a/apps/console/src/App.tsx +++ b/apps/console/src/App.tsx @@ -4,14 +4,10 @@ import { lazy, StrictMode, Suspense } from "react"; import { createRoot } from "react-dom/client"; import { HelmetProvider } from "react-helmet-async"; import { RelayEnvironmentProvider } from "react-relay"; -import { - BrowserRouter, - Route, - Routes, -} from "react-router"; +import { BrowserRouter, Route, Routes } from "react-router"; import "App.css"; import ErrorBoundary from "./components/ErrorBoundary"; -import AuthenticateLayout from "./layouts/AuthenticateLayout"; +import ConsoleLayout from "./layouts/ConsoleLayout"; import { RelayEnvironment } from "./RelayEnvironment"; posthog.init(process.env.POSTHOG_KEY!, { @@ -37,7 +33,7 @@ function App() { - }> + }> } /> } /> diff --git a/apps/console/src/components/AppSidebar.tsx b/apps/console/src/components/AppSidebar.tsx new file mode 100644 index 000000000..9226f925e --- /dev/null +++ b/apps/console/src/components/AppSidebar.tsx @@ -0,0 +1,169 @@ +import * as React from "react"; +import { Suspense, useEffect } from "react"; +import { + BookOpen, + Users, + ToyBrick, + Command, + LifeBuoy, + Send, +} from "lucide-react"; +import { + graphql, + PreloadedQuery, + usePreloadedQuery, + useQueryLoader, +} from "react-relay"; + +import { NavMain } from "@/components/NavMain"; +import { NavSecondary } from "@/components/NavSecondary"; +import { NavUser } from "@/components/NavUser"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar"; +import type { AppSidebarQuery as AppSidebarQueryType } from "./__generated__/AppSidebarQuery.graphql"; + +const staticData = { + user: { + name: "shadcn", + email: "m@example.com", + avatar: "/avatars/shadcn.jpg", + }, + navMain: [ + { + title: "Peoples", + url: "/peoples", + icon: Users, + }, + { + title: "Vendors", + url: "/vendors", + icon: ToyBrick, + }, + ], + navSecondary: [ + { + title: "Support", + url: "#", + icon: LifeBuoy, + }, + { + title: "Feedback", + url: "#", + icon: Send, + }, + ], +}; + +const AppSidebarQuery = graphql` + query AppSidebarQuery { + node(id: "AZSfP_xAcAC5IAAAAAAltA") { + id + ... on Organization { + name + logoUrl + createdAt + updatedAt + } + } + } +`; + +function AppSidebarContent({ + queryRef, + ...props +}: React.ComponentProps & { + queryRef: PreloadedQuery; +}) { + const data = usePreloadedQuery(AppSidebarQuery, queryRef); + const organization = data.node; + + return ( + + + + + + + {organization.name} +
+ + {organization.name} + + Enterprise +
+
+
+
+
+
+ + + + + + + +
+ ); +} + +function AppSidebarFallback(props: React.ComponentProps) { + return ( + + + + + + +
+
+ +
+ + Enterprise +
+
+ + + + + + + + + + + + + ); +} + +export function AppSidebar(props: React.ComponentProps) { + const [queryRef, loadQuery] = + useQueryLoader(AppSidebarQuery); + + useEffect(() => { + loadQuery({}); + }, [loadQuery]); + + if (!queryRef) { + return ; + } + + return ( + }> + + + ); +} diff --git a/apps/console/src/components/NavMain.tsx b/apps/console/src/components/NavMain.tsx new file mode 100644 index 000000000..9be7b119f --- /dev/null +++ b/apps/console/src/components/NavMain.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { ChevronRight, type LucideIcon } from "lucide-react"; + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from "@/components/ui/sidebar"; + +export function NavMain({ + items, +}: { + items: { + title: string; + url: string; + icon: LucideIcon; + isActive?: boolean; + items?: { + title: string; + url: string; + }[]; + }[]; +}) { + return ( + + Platform + + {items.map((item) => ( + + + + + + {item.title} + + + {item.items?.length ? ( + <> + + + + Toggle + + + + + {item.items?.map((subItem) => ( + + + + {subItem.title} + + + + ))} + + + + ) : null} + + + ))} + + + ); +} diff --git a/apps/console/src/components/NavSecondary.tsx b/apps/console/src/components/NavSecondary.tsx new file mode 100644 index 000000000..aee870402 --- /dev/null +++ b/apps/console/src/components/NavSecondary.tsx @@ -0,0 +1,40 @@ +import * as React from "react"; +import { type LucideIcon } from "lucide-react"; + +import { + SidebarGroup, + SidebarGroupContent, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar"; + +export function NavSecondary({ + items, + ...props +}: { + items: { + title: string; + url: string; + icon: LucideIcon; + }[]; +} & React.ComponentPropsWithoutRef) { + return ( + + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + + + ); +} diff --git a/apps/console/src/components/NavUser.tsx b/apps/console/src/components/NavUser.tsx new file mode 100644 index 000000000..2019263a4 --- /dev/null +++ b/apps/console/src/components/NavUser.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { + BadgeCheck, + Bell, + ChevronsUpDown, + CreditCard, + LogOut, + Sparkles, +} from "lucide-react"; + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar"; + +export function NavUser({ + user, +}: { + user: { + name: string; + email: string; + avatar: string; + }; +}) { + const { isMobile } = useSidebar(); + + return ( + + + + + + + + CN + +
+ {user.name} + {user.email} +
+ +
+
+ + +
+ + + CN + +
+ {user.name} + {user.email} +
+
+
+ + + + + Upgrade to Pro + + + + + + + Account + + + + Billing + + + + Notifications + + + + + + Log out + +
+
+
+
+ ); +} diff --git a/apps/console/src/components/__generated__/AppSidebarQuery.graphql.ts b/apps/console/src/components/__generated__/AppSidebarQuery.graphql.ts new file mode 100644 index 000000000..c046c6e0b --- /dev/null +++ b/apps/console/src/components/__generated__/AppSidebarQuery.graphql.ts @@ -0,0 +1,134 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type AppSidebarQuery$variables = Record; +export type AppSidebarQuery$data = { + readonly node: { + readonly createdAt?: any; + readonly id: string; + readonly name?: string; + readonly updatedAt?: any; + }; +}; +export type AppSidebarQuery = { + response: AppSidebarQuery$data; + variables: AppSidebarQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "kind": "Literal", + "name": "id", + "value": "AZSfP_xAcAC5IAAAAAAltA" + } +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v2 = { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null + } + ], + "type": "Organization", + "abstractKey": null +}; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "AppSidebarQuery", + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + (v2/*: any*/) + ], + "storageKey": "node(id:\"AZSfP_xAcAC5IAAAAAAltA\")" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "AppSidebarQuery", + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + (v1/*: any*/), + (v2/*: any*/) + ], + "storageKey": "node(id:\"AZSfP_xAcAC5IAAAAAAltA\")" + } + ] + }, + "params": { + "cacheID": "7afe463103b06ebb63c8db38cb99953c", + "id": null, + "metadata": {}, + "name": "AppSidebarQuery", + "operationKind": "query", + "text": "query AppSidebarQuery {\n node(id: \"AZSfP_xAcAC5IAAAAAAltA\") {\n __typename\n id\n ... on Organization {\n name\n createdAt\n updatedAt\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "3fbcab1da780d1530edf38d7b9ac4ec9"; + +export default node; diff --git a/apps/console/src/components/ui/avatar.tsx b/apps/console/src/components/ui/avatar.tsx new file mode 100644 index 000000000..444b1dbaa --- /dev/null +++ b/apps/console/src/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; + +import { cn } from "@/lib/utils"; + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/apps/console/src/components/ui/breadcrumb.tsx b/apps/console/src/components/ui/breadcrumb.tsx new file mode 100644 index 000000000..ecfc6a44d --- /dev/null +++ b/apps/console/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode; + } +>(({ ...props }, ref) =>