Skip to content

Commit

Permalink
feat: ✨ Implement dashboard feature.
Browse files Browse the repository at this point in the history
- Create dashboard page.
- Create dashboard navigation.
- Create dashboard sidebar.
- Create custom hook to manage dashboard sidebar state.
- Improved auth service.
- Update navbar and hint styling.

Users can now access their own dashboard to manage stream, account, community, chat and settings like software integration.
  • Loading branch information
RicardoGEsteves committed Dec 15, 2023
1 parent 0e0244f commit d961e7c
Show file tree
Hide file tree
Showing 15 changed files with 424 additions and 7 deletions.
2 changes: 1 addition & 1 deletion app/(browse)/_components/navbar/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const Actions = async () => {
<Button
size="sm"
variant="ghost"
className="text-muted-foreground"
className="text-muted-foreground hover:text-primary"
asChild
>
<Link href={`/u/${user.username}`}>
Expand Down
31 changes: 31 additions & 0 deletions app/(dashboard)/u/[username]/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { currentUser } from "@clerk/nextjs";

import { getUserByUsername } from "@/lib/user-service";
// import { StreamPlayer } from "@/components/stream-player";

interface CreatorPageProps {
params: {
username: string;
};
}

const CreatorPage = async ({ params }: CreatorPageProps) => {
const externalUser = await currentUser();
const user = await getUserByUsername(params.username);

if (!user || user.externalUserId !== externalUser?.id || !user.stream) {
throw new Error("Unauthorized");
}

return (
<div className="h-full">
{/* <StreamPlayer
user={user}
stream={user.stream}
isFollowing
/> */}
</div>
);
};

export default CreatorPage;
36 changes: 36 additions & 0 deletions app/(dashboard)/u/[username]/_components/container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";

import { useEffect } from "react";
import { useMediaQuery } from "usehooks-ts";

import { cn } from "@/lib/utils";
import { useCreatorSidebar } from "@/store/use-creator-sidebar";

interface ContainerProps {
children: React.ReactNode;
}

const Container = ({ children }: ContainerProps) => {
const { collapsed, onCollapse, onExpand } = useCreatorSidebar(
(state) => state
);
const matches = useMediaQuery(`(max-width: 1024px)`);

useEffect(() => {
if (matches) {
onCollapse();
} else {
onExpand();
}
}, [matches, onCollapse, onExpand]);

return (
<div
className={cn("flex-1", collapsed ? "ml-[70px]" : "ml-[70px] lg:ml-60")}
>
{children}
</div>
);
};

export default Container;
26 changes: 26 additions & 0 deletions app/(dashboard)/u/[username]/_components/navbar/actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Link from "next/link";
import { LogOut } from "lucide-react";
import { UserButton } from "@clerk/nextjs";

import { Button } from "@/components/ui/button";

const Actions = () => {
return (
<div className="flex items-center justify-end gap-x-2">
<Button
size="sm"
variant="ghost"
className="text-muted-foreground hover:text-primary"
asChild
>
<Link href="/">
<LogOut className="h-5 w-5 mr-2" />
Exit
</Link>
</Button>
<UserButton afterSignOutUrl="/" />
</div>
);
};

export default Actions;
14 changes: 14 additions & 0 deletions app/(dashboard)/u/[username]/_components/navbar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Actions from "./actions";
import Logo from "./logo";

const Navbar = () => {
return (
<nav className="fixed top-0 w-full h-20 z-[49] bg-primary-foreground px-2 lg:px-4 flex justify-between items-center shadow-sm">
<Logo />

<Actions />
</nav>
);
};

export default Navbar;
34 changes: 34 additions & 0 deletions app/(dashboard)/u/[username]/_components/navbar/logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Link from "next/link";
import Image from "next/image";
import { Poppins } from "next/font/google";

import { cn } from "@/lib/utils";

const font = Poppins({
subsets: ["latin"],
weight: ["200", "300", "400", "500", "600", "700", "800"],
});

const Logo = () => {
return (
<Link href="/">
<div className="flex items-center gap-x-4 hover:opacity-75 transition">
<div className="rounded-full mr-12 shrink-0 lg:mr-0 lg:shrink">
<Image
src="/s3mer.svg"
alt="S3mer logo"
height={50}
width={50}
/>
</div>

<div className={cn("hidden lg:block", font.className)}>
<p className="text-lg font-semibold">S3MER</p>
<p className="text-xs text-muted-foreground">Your S3MER Dashboard</p>
</div>
</div>
</Link>
);
};

export default Logo;
14 changes: 14 additions & 0 deletions app/(dashboard)/u/[username]/_components/sidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Toggle from "./toggle";
import Wrapper from "./wrapper";
import Navigation from "./navigation";

const Sidebar = () => {
return (
<Wrapper>
<Toggle />
<Navigation />
</Wrapper>
);
};

export default Sidebar;
52 changes: 52 additions & 0 deletions app/(dashboard)/u/[username]/_components/sidebar/nav-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";

import Link from "next/link";
import { LucideIcon } from "lucide-react";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { useCreatorSidebar } from "@/store/use-creator-sidebar";

interface NavItemProps {
icon: LucideIcon;
label: string;
href: string;
isActive: boolean;
}

const NavItem = ({ icon: Icon, label, href, isActive }: NavItemProps) => {
const { collapsed } = useCreatorSidebar((state) => state);

return (
<Button
asChild
variant="ghost"
className={cn(
"w-full h-12",
collapsed ? "justify-center" : "justify-start",
isActive && "bg-accent"
)}
>
<Link href={href}>
<div className="flex items-center gap-x-4">
<Icon className={cn("h-4 w-4", collapsed ? "mr-0" : "mr-2")} />
{!collapsed && <span>{label}</span>}
</div>
</Link>
</Button>
);
};

export default NavItem;

export const NavItemSkeleton = () => {
return (
<li className="flex items-center gap-x-4 px-3 py-2">
<Skeleton className="min-h-[48px] min-w-[48px] rounded-md" />
<div className="flex-1 hidden lg:block">
<Skeleton className="h-6" />
</div>
</li>
);
};
61 changes: 61 additions & 0 deletions app/(dashboard)/u/[username]/_components/sidebar/navigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use client";

import { useUser } from "@clerk/nextjs";
import { usePathname } from "next/navigation";
import { Fullscreen, KeyRound, MessageSquare, Users } from "lucide-react";

import NavItem, { NavItemSkeleton } from "./nav-item";

const Navigation = () => {
const pathname = usePathname();
const { user } = useUser();

const routes = [
{
label: "Stream",
href: `/u/${user?.username}`,
icon: Fullscreen,
},
{
label: "Keys",
href: `/u/${user?.username}/keys`,
icon: KeyRound,
},
{
label: "Chat",
href: `/u/${user?.username}/chat`,
icon: MessageSquare,
},
{
label: "Community",
href: `/u/${user?.username}/community`,
icon: Users,
},
];

if (!user?.username) {
return (
<ul className="space-y-2">
{[...Array(4)].map((_, i) => (
<NavItemSkeleton key={i} />
))}
</ul>
);
}

return (
<ul className="space-y-2 px-2 pt-4 lg:pt-0">
{routes.map((route) => (
<NavItem
key={route.href}
label={route.label}
icon={route.icon}
href={route.href}
isActive={pathname === route.href}
/>
))}
</ul>
);
};

export default Navigation;
57 changes: 57 additions & 0 deletions app/(dashboard)/u/[username]/_components/sidebar/toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use client";

import { ArrowLeftFromLine, ArrowRightFromLine } from "lucide-react";

import Hint from "@/components/hint";
import { Button } from "@/components/ui/button";
import { useCreatorSidebar } from "@/store/use-creator-sidebar";

const Toggle = () => {
const { collapsed, onExpand, onCollapse } = useCreatorSidebar(
(state) => state
);

const label = collapsed ? "Expand" : "Collapse";

return (
<>
{collapsed && (
<div className="w-full hidden lg:flex items-center justify-center pt-4 mb-4">
<Hint
label={label}
side="right"
asChild
>
<Button
onClick={onExpand}
variant="ghost"
className="h-auto p-2"
>
<ArrowRightFromLine className="h-4 w-4" />
</Button>
</Hint>
</div>
)}
{!collapsed && (
<div className="p-3 pl-6 mb-2 hidden lg:flex items-center w-full">
<p className="font-semibold text-foreground">Dashboard</p>
<Hint
label={label}
side="right"
asChild
>
<Button
onClick={onCollapse}
variant="ghost"
className="h-auto p-2 ml-auto"
>
<ArrowLeftFromLine className="h-4 w-4" />
</Button>
</Hint>
</div>
)}
</>
);
};

export default Toggle;
25 changes: 25 additions & 0 deletions app/(dashboard)/u/[username]/_components/sidebar/wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";

import { cn } from "@/lib/utils";
import { useCreatorSidebar } from "@/store/use-creator-sidebar";

interface WrapperProps {
children: React.ReactNode;
}

const Wrapper = ({ children }: WrapperProps) => {
const { collapsed } = useCreatorSidebar((state) => state);

return (
<aside
className={cn(
"fixed left-0 flex flex-col w-[70px] lg:w-60 h-full bg-secondary/40 z-50",
collapsed && "lg:w-[70px]"
)}
>
{children}
</aside>
);
};

export default Wrapper;
Loading

0 comments on commit d961e7c

Please sign in to comment.