mirror of
https://github.com/bartvdbraak/omnidash.git
synced 2025-04-28 16:01: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