Initial Commit

This commit is contained in:
Ahmad 2024-02-14 21:30:10 -05:00
commit f3e2f01bd7
No known key found for this signature in database
GPG key ID: 8FD8A93530D182BF
150 changed files with 13612 additions and 0 deletions

View file

@ -0,0 +1,67 @@
import { Plus } from "lucide-react";
import { OrganizationSwitcher, UserButton } from "@clerk/nextjs";
import { Logo } from "@/components/logo";
import { Button } from "@/components/ui/button";
import { FormPopover } from "@/components/form/form-popover";
import { MobileSidebar } from "./mobile-sidebar";
export const Navbar = () => {
return (
<nav className="fixed z-50 top-0 px-4 w-full h-14 border-b shadow-sm bg-white flex items-center">
<MobileSidebar />
<div className="flex items-center gap-x-4">
<div className="hidden md:flex">
<Logo />
</div>
<FormPopover align="start" side="bottom" sideOffset={18}>
<Button
variant="primary"
size="sm"
className="rounded-sm hidden md:block h-auto py-1.5 px-2"
>
Create
</Button>
</FormPopover>
<FormPopover>
<Button
variant="primary"
size="sm"
className="rounded-sm block md:hidden"
>
<Plus className="h-4 w-4" />
</Button>
</FormPopover>
</div>
<div className="ml-auto flex items-center gap-x-2">
<OrganizationSwitcher
hidePersonal
afterCreateOrganizationUrl="/organization/:id"
afterLeaveOrganizationUrl="/org-select"
afterSelectOrganizationUrl="/organization/:id"
appearance={{
elements: {
rootBox: {
display: "flex",
justifyContent: "center",
alignItems: "center",
},
},
}}
/>
<UserButton
afterSignOutUrl="/"
appearance={{
elements: {
avatarBox: {
height: 30,
width: 30,
},
},
}}
/>
</div>
</nav>
);
};

View file

@ -0,0 +1,49 @@
"use client";
import { Menu } from "lucide-react";
import { usePathname } from "next/navigation";
import { useMobileSidebar } from "@/hooks/use-mobile-sidebar";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import { Sidebar } from "./sidebar";
export const MobileSidebar = () => {
const pathname = usePathname();
const [isMounted, setIsMounted] = useState(false);
const onOpen = useMobileSidebar((state) => state.onOpen);
const onClose = useMobileSidebar((state) => state.onClose);
const isOpen = useMobileSidebar((state) => state.isOpen);
useEffect(() => {
setIsMounted(true);
}, []);
useEffect(() => {
onClose();
}, [pathname, onClose]);
if (!isMounted) {
return null;
}
return (
<>
<Button
onClick={onOpen}
className="block md:hidden mr-2"
variant="ghost"
size="sm"
>
<Menu className="h-4 w-4" />
</Button>
<Sheet open={isOpen} onOpenChange={onClose}>
<SheetContent side="left" className="p-2 pt-10">
<Sidebar storageKey="t-sidebar-mobile-state" />
</SheetContent>
</Sheet>
</>
);
};

View file

@ -0,0 +1,117 @@
"use client";
import Image from "next/image";
import { Activity, CreditCard, Layout, Settings } from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
import {
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
export type Organization = {
id: string;
slug: string;
imageUrl: string;
name: string;
};
interface NavItemsProps {
isExpanded: boolean;
isActive: boolean;
organization: Organization;
onExpand: (id: string) => void;
}
export const NavItem = ({
isExpanded,
isActive,
organization,
onExpand,
}: NavItemsProps) => {
const router = useRouter();
const pathname = usePathname();
const routes = [
{
label: "Boards",
icon: <Layout className="h-4 w-4 mr-2" />,
href: `/organization/${organization.id}`,
},
{
label: "Activity",
icon: <Activity className="h-4 w-4 mr-2" />,
href: `/organization/${organization.id}/activity`,
},
{
label: "Settings",
icon: <Settings className="h-4 w-4 mr-2" />,
href: `/organization/${organization.id}/settings`,
},
{
label: "Billing",
icon: <CreditCard className="h-4 w-4 mr-2" />,
href: `/organization/${organization.id}/billing`,
},
];
const onClick = (href: string) => {
router.push(href);
};
return (
<AccordionItem value={organization.id} className="border-none">
<AccordionTrigger
onClick={() => onExpand(organization.id)}
className={cn(
"flex items-center gap-x-2 p-1.5 text-neutral-700 rounded-md hover:bg-neutral-500/10 transition text-start no-underline hover:no-underline",
isActive && !isExpanded && "bg-sky-500/10 text-sky-700"
)}
>
<div className="flex items-center gap-x-2">
<div className="w-7 h-7 relative">
<Image
fill
src={organization.imageUrl}
alt={organization.name}
className="rounded-sm object-cover"
/>
</div>
<span className="font-medium text-sm">{organization.name}</span>
</div>
</AccordionTrigger>
<AccordionContent className="pt-1 text-neutral-700">
{routes.map((route) => (
<Button
key={route.href}
size="sm"
onClick={() => onClick(route.href)}
className={cn(
"w-full font-normal justify-start pl-10 mb-1",
pathname === route.href && "bg-sky-500/10 text-sky-700"
)}
variant="ghost"
>
{route.icon}
{route.label}
</Button>
))}
</AccordionContent>
</AccordionItem>
);
};
NavItem.Skeleton = function SkeletonNavItem() {
return (
<div className="flex items-center gap-x-2">
<div className="w-10 h-10 relative shrink-0">
<Skeleton className="h-full w-full absolute" />
</div>
<Skeleton className="h-4 w-full" />
</div>
);
};

View file

@ -0,0 +1,95 @@
"use client";
import Link from "next/link";
import { Plus } from "lucide-react";
import { useLocalStorage } from "usehooks-ts";
import { useOrganizationList, useOrganization } from "@clerk/nextjs";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Accordion } from "@/components/ui/accordion";
import { Skeleton } from "@/components/ui/skeleton";
import { NavItem, Organization } from "./nav-item";
interface SidebarProps {
storageKey?: string;
}
export const Sidebar = ({ storageKey = "t-sidebar-state" }: SidebarProps) => {
const [expanded, setExpanded] = useLocalStorage<Record<string, any>>(
storageKey,
{}
);
const { organization: activeOrganization, isLoaded: isLoadedOrg } =
useOrganization();
const { userMemberships, isLoaded: isLoadedOrgList } = useOrganizationList({
userMemberships: { infinite: true },
});
const defaultAccordionValue: string[] = Object.keys(expanded).reduce(
(acc: string[], key: string) => {
if (expanded[key]) {
acc.push(key);
}
return acc;
},
[]
);
const onExpand = (id: string) => {
setExpanded((curr) => ({
...curr,
[id]: !expanded[id],
}));
};
if (!isLoadedOrgList || !isLoadedOrg || userMemberships.isLoading) {
return (
<>
<div className="flex items-center justify-between mb-2">
<Skeleton className="h-10 w-[50%]" />
<Skeleton className="h-10 w-10" />
</div>
<div className="space-y-2">
<NavItem.Skeleton />
<NavItem.Skeleton />
<NavItem.Skeleton />
</div>
</>
);
}
return (
<>
<div className="font-medium text-xs flex items-center mb-1">
<span className="pl-4">Workspaces</span>
<Button
asChild
type="button"
size="icon"
variant="ghost"
className="ml-auto"
>
<Link href="/select-org">
<Plus className="h-4 w-4" />
</Link>
</Button>
</div>
<Accordion
type="multiple"
defaultValue={defaultAccordionValue}
className="space-y-2"
>
{userMemberships.data.map(({ organization }) => (
<NavItem
key={organization.id}
isActive={activeOrganization?.id === organization.id}
isExpanded={expanded[organization.id]}
organization={organization as Organization}
onExpand={onExpand}
/>
))}
</Accordion>
</>
);
};