mirror of
https://github.com/ahmadk953/tasko.git
synced 2025-05-01 03:09:34 +00:00
Initial Commit
This commit is contained in:
commit
f3e2f01bd7
150 changed files with 13612 additions and 0 deletions
67
app/(platform)/(dashboard)/_components/Navbar.tsx
Normal file
67
app/(platform)/(dashboard)/_components/Navbar.tsx
Normal 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>
|
||||
);
|
||||
};
|
49
app/(platform)/(dashboard)/_components/mobile-sidebar.tsx
Normal file
49
app/(platform)/(dashboard)/_components/mobile-sidebar.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
117
app/(platform)/(dashboard)/_components/nav-item.tsx
Normal file
117
app/(platform)/(dashboard)/_components/nav-item.tsx
Normal 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>
|
||||
);
|
||||
};
|
95
app/(platform)/(dashboard)/_components/sidebar.tsx
Normal file
95
app/(platform)/(dashboard)/_components/sidebar.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue