mirror of
https://github.com/bartvdbraak/omnidash.git
synced 2025-04-27 15:31:21 +00:00
feat: Add pages/components for authenticated flow
This commit is contained in:
parent
130a4932e6
commit
6c4b88cff7
7 changed files with 478 additions and 0 deletions
84
app/(authenticated)/(app)/DesktopSidebar.tsx
Normal file
84
app/(authenticated)/(app)/DesktopSidebar.tsx
Normal 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>
|
||||
);
|
||||
};
|
107
app/(authenticated)/(app)/MobileSidebar.tsx
Normal file
107
app/(authenticated)/(app)/MobileSidebar.tsx
Normal 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>
|
||||
);
|
||||
};
|
99
app/(authenticated)/(app)/TeamSwitcher.tsx
Normal file
99
app/(authenticated)/(app)/TeamSwitcher.tsx
Normal 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>
|
||||
);
|
||||
};
|
28
app/(authenticated)/(app)/channelLink.tsx
Normal file
28
app/(authenticated)/(app)/channelLink.tsx
Normal 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>
|
||||
);
|
||||
};
|
32
app/(authenticated)/(app)/layout.tsx
Normal file
32
app/(authenticated)/(app)/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
9
app/(authenticated)/(app)/overview/loading.tsx
Normal file
9
app/(authenticated)/(app)/overview/loading.tsx
Normal 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>
|
||||
);
|
||||
}
|
119
app/(authenticated)/(app)/overview/page.tsx
Normal file
119
app/(authenticated)/(app)/overview/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue