feat: Add pages/components for authenticated flow

This commit is contained in:
Bart van der Braak 2023-06-08 00:39:28 +02:00
parent 130a4932e6
commit 6c4b88cff7
7 changed files with 478 additions and 0 deletions

View file

@ -0,0 +1,84 @@
import { Logo } from "@/components/logo";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { BarChart, Database, FileKey, Filter, FormInput, Home } from "lucide-react";
import { ChannelLink } from "./channelLink";
import { TeamSwitcher } from "./TeamSwitcher";
import Link from "next/link";
type Props = {
navigation: {
href: string;
external?: boolean;
label: string;
}[];
channels: {
name: string;
}[];
};
export const DesktopSidebar: React.FC<Props> = ({ navigation, channels }) => {
return (
<aside className="relative hidden min-h-screen pb-12 border-r lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col border-white/10">
<Link
href="/overview"
className="flex items-center gap-2 px-8 py-6 text-2xl font-semibold tracking-tight duration-200 stroke-zinc-800 dark:text-zinc-200 dark:stroke-zinc-500 dark:hover:stroke-white hover:stroke-zinc-700 hover:text-zinc-700 dark:hover:text-white"
>
<Logo className="w-8 h-8 duration-200 " />
Omnidash
</Link>
<div className="space-y-4">
<div className="px-6 py-2">
<h2 className="px-2 mb-2 text-lg font-semibold tracking-tight">{/* Events */}</h2>
<div className="space-y-1">
<Link href="/overview">
<Button variant="ghost" size="sm" className="justify-start w-full">
<Home className="w-4 h-4 mr-2" />
Overview
</Button>
</Link>
<Link href="/keys">
<Button variant="ghost" size="sm" className="justify-start w-full">
<FileKey className="w-4 h-4 mr-2" />
API Keys
</Button>
</Link>
<Link href="/channels">
<Button variant="ghost" size="sm" className="justify-start w-full">
<Database className="w-4 h-4 mr-2" />
Channels
</Button>
</Link>
<Button variant="ghost" disabled size="sm" className="justify-start w-full">
<Filter className="w-4 h-4 mr-2" />
Filter
</Button>
<Button variant="ghost" disabled size="sm" className="justify-start w-full">
<BarChart className="w-4 h-4 mr-2" />
Analytics
</Button>
</div>
</div>
<div className="py-2">
<h2 className="relative px-8 text-lg font-semibold tracking-tight">Channels</h2>
<ScrollArea className="h-[230px] px-4">
<div className="p-2 space-y-1">
{channels
.sort((a, b) => a.name.localeCompare(b.name))
.map((channel) => (
<ChannelLink
key={channel.name}
href={`/channels/${channel.name}`}
channelName={channel.name}
/>
))}
</div>
</ScrollArea>
</div>
</div>
<div className="absolute inset-x-0 mx-6 bottom-8">
<TeamSwitcher />
</div>
</aside>
);
};

View file

@ -0,0 +1,107 @@
"use client";
import { Logo } from "@/components/logo";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { BarChart, Database, FileKey, Filter, FormInput, Home, Menu } from "lucide-react";
import { ChannelLink } from "./channelLink";
import { TeamSwitcher } from "./TeamSwitcher";
import Link from "next/link";
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
type Props = {
navigation: {
href: string;
external?: boolean;
label: string;
}[];
channels: {
name: string;
}[];
};
export const MobileSidebar: React.FC<Props> = ({ navigation, channels }) => {
return (
<div className="lg:hidden">
<Sheet>
<div className="sticky top-0 z-40 flex items-center justify-end w-full px-4 py-4 bg-zinc-950 gap-x-6 sm:px-6 lg:hidden">
<SheetTrigger>
<Menu />
</SheetTrigger>
</div>
<SheetContent position="bottom" size="content">
<SheetHeader>
<SheetTitle className="flex items-center justify-center gap-2">
{" "}
<Logo className="w-8 h-8 stroke-zinc-300" />
Omnidash
</SheetTitle>
{/* <SheetDescription>
Make changes to your profile here. Click save when you're done.
</SheetDescription> */}
</SheetHeader>
<div className="space-y-4">
<div className="px-6 py-2">
<h2 className="px-2 mb-2 text-lg font-semibold tracking-tight">{/* Events */}</h2>
<div className="space-y-1">
<Link href="/overview">
<Button variant="ghost" size="sm" className="justify-start w-full">
<Home className="w-4 h-4 mr-2" />
Overview
</Button>
</Link>
<Link href="/keys">
<Button variant="ghost" size="sm" className="justify-start w-full">
<FileKey className="w-4 h-4 mr-2" />
API Keys
</Button>
</Link>
<Link href="/channels">
<Button variant="ghost" size="sm" className="justify-start w-full">
<Database className="w-4 h-4 mr-2" />
Channels
</Button>
</Link>
<Button variant="ghost" disabled size="sm" className="justify-start w-full">
<Filter className="w-4 h-4 mr-2" />
Filter
</Button>
<Button variant="ghost" disabled size="sm" className="justify-start w-full">
<BarChart className="w-4 h-4 mr-2" />
Analytics
</Button>
</div>
</div>
<div className="py-2">
<h2 className="relative px-8 text-lg font-semibold tracking-tight">Events</h2>
<ScrollArea className="h-[230px] px-4">
<div className="p-2 space-y-1">
{channels.map((channel) => (
<ChannelLink
key={channel.name}
href={`/channels/${channel.name}`}
channelName={channel.name}
/>
))}
</div>
</ScrollArea>
</div>
</div>
<SheetFooter>
<TeamSwitcher />
</SheetFooter>
</SheetContent>
</Sheet>
</div>
);
};

View file

@ -0,0 +1,99 @@
"use client";
import {
DropdownMenuTrigger,
DropdownMenu,
DropdownMenuContent,
DropdownMenuCheckboxItem,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
} from "@/components/ui/dropdown-menu";
import { Check, ChevronsUpDown, Plus, Key, Book, LogOut, Rocket } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Loading } from "@/components/loading";
import { cn } from "@/lib/utils";
import { useAuth, useOrganization, useOrganizationList, useUser } from "@clerk/clerk-react";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { AvatarFallback } from "@radix-ui/react-avatar";
type Props = {};
export const TeamSwitcher: React.FC<Props> = (): JSX.Element => {
const { setActive, organizationList } = useOrganizationList();
const { organization: currentOrg } = useOrganization();
const { signOut } = useAuth();
const { user } = useUser();
const router = useRouter();
const [loading, setLoading] = useState(false);
async function changeOrg(id: string | null) {
if (!setActive) {
return;
}
try {
setLoading(true);
await setActive({ organization: id });
router.refresh();
} finally {
setLoading(false);
}
}
return (
<DropdownMenu>
{loading ? (
<Loading />
) : (
<DropdownMenuTrigger className="flex items-center justify-between w-full px-2 py-1 rounded gap-4 hover:bg-zinc-100 dark:hover:bg-zinc-700">
<div className="flex items-center justify-start w-full gap-4 ">
<Avatar>
{user?.profileImageUrl ? (
<AvatarImage src={user.profileImageUrl} alt={user.username ?? "Profile picture"} />
) : null}
<AvatarFallback className="flex items-center justify-center w-8 h-8 overflow-hidden border rounded-md bg-zinc-100 border-zinc-500 text-zinc-700">
{(currentOrg?.slug ?? user?.username ?? "").slice(0, 2).toUpperCase() ?? "P"}
</AvatarFallback>
</Avatar>
<span>{currentOrg?.name ?? "Personal"}</span>
</div>
{/* <PlanBadge plan={currentTeam?.plan ?? "DISABLED"} /> */}
<ChevronsUpDown className="w-4 h-4" />
</DropdownMenuTrigger>
)}
<DropdownMenuContent className="w-full lg:w-56" align="end" forceMount>
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<button
onClick={async () => {
await signOut();
router.refresh();
}}
className="w-full"
>
<LogOut className="w-4 h-4 mr-2" />
<span>Sign out</span>
</button>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};

View file

@ -0,0 +1,28 @@
"use client";
import Link from "next/link";
import { useSelectedLayoutSegments } from "next/navigation";
import { Hash } from "lucide-react";
import { Button } from "@/components/ui/button";
type Props = {
href: string;
channelName: string | null;
};
export const ChannelLink: React.FC<Props> = ({ href, channelName }) => {
const isActive = channelName === useSelectedLayoutSegments().at(1);
return (
<Link href={href}>
<Button
variant={isActive ? "subtle" : "ghost"}
size="sm"
className="justify-start w-full font-normal"
>
<Hash className="w-4 h-4 mr-2" />
{channelName}
</Button>
</Link>
);
};

View file

@ -0,0 +1,32 @@
import Link from "next/link";
import { BarChart, FileKey, Filter, FormInput, Keyboard, Menu, Tornado } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { TeamSwitcher } from "./TeamSwitcher";
import { auth } from "@clerk/nextjs/app-beta";
import { notFound } from "next/navigation";
import { ChannelLink } from "./channelLink";
import { Logo } from "@/components/logo";
import { DesktopSidebar } from "./DesktopSidebar";
import { MobileNav } from "@/components/mobile-nav";
import { MobileSidebar } from "./MobileSidebar";
import { getTenantId } from "@/lib/auth";
import { Fragment } from "react";
interface LayoutProps {
children: React.ReactNode;
}
export default async function Layout({ children }: LayoutProps) {
const tenantId = getTenantId();
return (
<Fragment>
<DesktopSidebar channels={[]} navigation={[]} />
<MobileSidebar channels={[]} navigation={[]} />
<div className=" lg:pl-72">{children}</div>
</Fragment>
);
}

View file

@ -0,0 +1,9 @@
import { Loading as Spinner } from "@/components/loading";
export default function Loading() {
return (
<div className="flex items-center justify-center w-full h-screen">
<Spinner className="text-white" />
</div>
);
}

View file

@ -0,0 +1,119 @@
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import { auth } from "@clerk/nextjs/app-beta";
import { PageHeader } from "@/components/page-header";
import { useUser } from "@clerk/clerk-react";
import { Fragment } from "react";
import { cn } from "@/lib/utils";
import { getTenantId } from "@/lib/auth";
export default async function Page(_props: {
params: { tenantSlug: string };
}) {
const tenantId = getTenantId();
const stats: {
label: string;
value: string;
}[] = [
{
label: "Total Channels",
value: '0',
},
{
label: "Total Events (7 days)",
value: '0',
},
];
return (
<main>
<div className="relative overflow-hidden isolate">
{/* Stats */}
<div className="border-b border-b-white/10 ">
<div className="flex flex-col items-start justify-between h-16 px-4 py-4 border-b bg-primary-900 gap-x-8 gap-y-4 sm:flex-row sm:items-center sm:px-6 lg:px-8 border-white/10">
<div>
<div className="flex items-center gap-x-3 ">
{/* <div className="flex-none p-1 text-green-400 rounded-full bg-green-400/10">
<div className="w-2 h-2 rounded-full bg-current" />
</div> */}
<h1 className="flex text-base gap-x-2 leading-7">
<span className="font-semibold text-white">
{"Personal Account"}
</span>
</h1>
</div>
{/* <p className="mt-2 text-xs leading-6 text-zinc-400">{channel.description}</p> */}
</div>
<div className="flex-none order-first px-2 py-1 text-xs font-medium rounded-full bg-rose-400/10 text-rose-400 ring-1 ring-inset ring-rose-400/30 sm:order-none">
test
</div>
</div>
<dl
className={cn(
"grid grid-cols-1 bg-zinc-700/10 sm:grid-cols-2 border-b border-white/10 h-32",
{
"lg:grid-cols-2": stats.length === 2,
"lg:grid-cols-3": stats.length === 3,
"lg:grid-cols-4": stats.length >= 4,
},
)}
>
{" "}
{stats.map((stat, statIdx) => (
<div
key={stat.label}
className={cn(
statIdx % 2 === 1 ? "sm:border-l" : statIdx === 2 ? "lg:border-l" : "",
"flex items-baseline flex-wrap justify-between gap-y-2 gap-x-4 border-t border-zinc-100/5 px-4 py-10 sm:px-6 lg:border-t-0 xl:px-8",
)}
>
<dt className="text-sm font-medium leading-6 text-zinc-500">{stat.label}</dt>
{/* <dd
className={cn(
stat.changeType === 'negative' ? 'text-rose-600' : 'text-zinc-700',
'text-xs font-medium'
)}
>
{stat.change}
</dd> */}
<dd className="flex-none w-full text-3xl font-medium tracking-tight leading-10 text-zinc-100">
{stat.value}
</dd>
</div>
))}
</dl>
</div>
</div>
<div className="py-16 space-y-16 xl:space-y-20">
{/* Recent activity table */}
<div>
<div className="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
<h2 className="max-w-2xl mx-auto text-base font-semibold leading-6 text-zinc-100 lg:mx-0 lg:max-w-none">
Recent events
</h2>
</div>
<div className="mt-6 overflow-x-hidden overflow-y-scroll border-t border-zinc-900">
<div className="mx-auto max-w-7xl ">
<div className="max-w-2xl mx-auto lg:mx-0 lg:max-w-none">
<table className="w-full text-left ">
<thead className="sr-only">
<tr>
<th>Event</th>
<th className="hidden sm:table-cell">Content</th>
<th>More details</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</main>
);
}