Skip to content

Commit

Permalink
feat: Creator Dashboard (#13)
Browse files Browse the repository at this point in the history
---

<details open="true"><summary>Generated summary (powered by <a href="https://app.graphite.dev">Graphite</a>)</summary>

> # Pull Request Description
> 
> ## TL;DR
> This pull request adds new files and modifies existing code to implement a CreatorPage component, a Container component, a navbar component, a sidebar component, and a navigation component in a dashboard application. It also includes various styling and layout components, as well as functions for authentication and navigation.
> 
> ## What changed
> - Added a new file called "page.tsx" that contains code for a CreatorPage component that retrieves user data and checks for authorization.
> - Added a new file called "container.tsx" that defines a React component called "Container" which adjusts its layout based on the screen size.
> - Added a new file called "nav-item.tsx" to the sidebar component, which renders a navigation item with an icon, label, and link.
> - Modified existing code to include a navbar component with a logo and actions, such as a logout button and a user button.
> - Modified existing code to include a sidebar component with a link to the homepage, an image, and text displaying the name of the dashboard and a description.
> - Modified existing code to include a navigation component with a logo and actions, such as a logout button and a user button.
> - Modified existing code to include a toggle button for expanding or collapsing the sidebar.
> - Modified existing code to include a collapsible dashboard component with a hint label and a button to expand or collapse the dashboard.
> - Modified existing code to include a sidebar wrapper component with conditional rendering based on the "collapsed" state.
> 
> ## How to test
> 1. Clone the repository.
> 2. Checkout the branch containing this pull request.
> 3. Run the application and navigate to the dashboard page.
> 4. Verify that the CreatorPage component retrieves user data and checks for authorization.
> 5. Verify that the Container component adjusts its layout based on the screen size.
> 6. Verify that the navbar component includes a logo and actions, such as a logout button and a user button.
> 7. Verify that the sidebar component includes a link to the homepage, an image, and text displaying the name of the dashboard and a description.
> 8. Verify that the navigation component includes a logo and actions, such as a logout button and a user button.
> 9. Verify that the toggle button expands or collapses the sidebar.
> 10. Verify that the collapsible dashboard component displays a hint label and a button to expand or collapse the dashboard.
> 11. Verify that the sidebar wrapper component renders with conditional rendering based on the "collapsed" state.
> 
> ## Why make this change
> This change is made to enhance the functionality and user experience of the dashboard application. The addition of the CreatorPage component allows for retrieving user data and checking authorization. The Container component adjusts its layout based on the screen size, providing a responsive design. The navbar, sidebar, and navigation components improve navigation and provide easy access to important actions. The toggle button and collapsible dashboard component enhance the usability of the sidebar. The sidebar wrapper component improves the overall layout and styling of the dashboard page.
</details>
  • Loading branch information
Zaid-maker authored Dec 21, 2023
2 parents 3cf2705 + 55915d9 commit e3b8872
Show file tree
Hide file tree
Showing 14 changed files with 379 additions and 19 deletions.
21 changes: 21 additions & 0 deletions app/(dashboard)/u/[username]/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getUserByUsername } from "@/lib/user-service";
import { currentUser } from "@clerk/nextjs";

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) {
throw new Error("Unauthorized");
}

return <div className="h-full">CreatorPage</div>;
};

export default CreatorPage;
35 changes: 35 additions & 0 deletions app/(dashboard)/u/[username]/_components/container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"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;
}

export 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>
);
};
23 changes: 23 additions & 0 deletions app/(dashboard)/u/[username]/_components/navbar/actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Button } from "@/components/ui/button";
import { UserButton } from "@clerk/nextjs";
import { LogOut } from "lucide-react";
import Link from "next/link";

export 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>
);
};
12 changes: 12 additions & 0 deletions app/(dashboard)/u/[username]/_components/navbar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from "react";
import { Logo } from "./logo";
import { Actions } from "./actions";

export const Navbar = () => {
return (
<nav className="fixed top-0 w-full h-20 z-[49] bg-[#252731] px-2 lg:px-4 flex justify-between items-center shadow-sm">
<Logo />
<Actions />
</nav>
);
};
25 changes: 25 additions & 0 deletions app/(dashboard)/u/[username]/_components/navbar/logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { cn } from "@/lib/utils";
import { Poppins } from "next/font/google";
import Image from "next/image";
import Link from "next/link";

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

export const Logo = () => {
return (
<Link href="/">
<div className="flex items-center gap-x-4 hover:opacity-75 transition">
<div className="bg-white rounded-full p-1 mr-12 shrink-0 lg:mr-0 lg:shrink">
<Image src="/spooky.svg" alt="gamehub" height="32" width="32" />
</div>
<div className={cn(font.className, "hidden lg:block")}>
<p className="text-lg font-semibold">Gamehub</p>
<p className="text-xs text-muted-foreground">Creator Dashboard</p>
</div>
</div>
</Link>
);
};
12 changes: 12 additions & 0 deletions app/(dashboard)/u/[username]/_components/sidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Navigation } from "./navigation";
import { Toggle } from "./toggle";
import { Wrapper } from "./wrapper";

export const Sidebar = async () => {
return (
<Wrapper>
<Toggle />
<Navigation />
</Wrapper>
);
};
54 changes: 54 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,54 @@
"use client";

import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { useCreatorSidebar } from "@/store/use-creator-sidebar";
import { LucideIcon } from "lucide-react";
import Link from "next/link";

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

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

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

export 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,
},
];

return (
<div 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}
/>
))}
</div>
);
};
42 changes: 42 additions & 0 deletions app/(dashboard)/u/[username]/_components/sidebar/toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client";

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

export 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-primary">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>
)}
</>
);
};
24 changes: 24 additions & 0 deletions app/(dashboard)/u/[username]/_components/sidebar/wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";

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

interface WrapperProps {
children: React.ReactNode;
}

export 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-background border-r border-[#2D2E35] z-50",
collapsed && "lg:w-[70px]"
)}
>
{children}
</aside>
);
};
33 changes: 33 additions & 0 deletions app/(dashboard)/u/[username]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getSelfByUsername } from "@/lib/auth-service";
import { redirect } from "next/navigation";
import React from "react";
import { Container } from "./_components/container";
import { Navbar } from "./_components/navbar";
import { Sidebar } from "./_components/sidebar";

interface CreatorLayoutProps {
params: {
username: string;
};
children: React.ReactNode;
}

const CreatorLayout = async ({ params, children }: CreatorLayoutProps) => {
const self = await getSelfByUsername(params.username);

if (!self) {
redirect("/");
}

return (
<>
<Navbar />
<div className="flex h-full pt-20">
<Sidebar />
<Container>{children}</Container>
</div>
</>
);
};

export default CreatorLayout;
50 changes: 37 additions & 13 deletions lib/auth-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,45 @@ import { currentUser } from "@clerk/nextjs";
import { db } from "./db";

export const getSelf = async () => {
const self = await currentUser();
const self = await currentUser();

if (!self || !self.username) {
throw new Error("Unauthorized");
}
if (!self || !self.username) {
throw new Error("Unauthorized");
}

const user = await db.user.findUnique({
where: {
externalUserId: self.id,
},
});
const user = await db.user.findUnique({
where: {
externalUserId: self.id,
},
});

if (!user) {
throw new Error("Not Found");
}
if (!user) {
throw new Error("Not Found");
}

return user;
return user;
};

export const getSelfByUsername = async (username: string) => {
const self = await currentUser();

if (!self || !self.username) {
throw new Error("Unauthorized");
}

const user = await db.user.findUnique({
where: {
username,
},
});

if (!user) {
throw new Error("User not found");
}

if (self.username !== user.username) {
throw new Error("Unauthorized");
}

return self;
};
Loading

0 comments on commit e3b8872

Please sign in to comment.