diff --git a/app/(dashboard)/u/[username]/(home)/page.tsx b/app/(dashboard)/u/[username]/(home)/page.tsx new file mode 100644 index 0000000..ec2f080 --- /dev/null +++ b/app/(dashboard)/u/[username]/(home)/page.tsx @@ -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
CreatorPage
; +}; + +export default CreatorPage; diff --git a/app/(dashboard)/u/[username]/_components/container.tsx b/app/(dashboard)/u/[username]/_components/container.tsx new file mode 100644 index 0000000..d109568 --- /dev/null +++ b/app/(dashboard)/u/[username]/_components/container.tsx @@ -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 ( +
+ {children} +
+ ); +}; diff --git a/app/(dashboard)/u/[username]/_components/navbar/actions.tsx b/app/(dashboard)/u/[username]/_components/navbar/actions.tsx new file mode 100644 index 0000000..21cf8c6 --- /dev/null +++ b/app/(dashboard)/u/[username]/_components/navbar/actions.tsx @@ -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 ( +
+ + +
+ ); +}; diff --git a/app/(dashboard)/u/[username]/_components/navbar/index.tsx b/app/(dashboard)/u/[username]/_components/navbar/index.tsx new file mode 100644 index 0000000..308e336 --- /dev/null +++ b/app/(dashboard)/u/[username]/_components/navbar/index.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { Logo } from "./logo"; +import { Actions } from "./actions"; + +export const Navbar = () => { + return ( + + ); +}; diff --git a/app/(dashboard)/u/[username]/_components/navbar/logo.tsx b/app/(dashboard)/u/[username]/_components/navbar/logo.tsx new file mode 100644 index 0000000..3e6ec04 --- /dev/null +++ b/app/(dashboard)/u/[username]/_components/navbar/logo.tsx @@ -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 ( + +
+
+ gamehub +
+
+

Gamehub

+

Creator Dashboard

+
+
+ + ); +}; diff --git a/app/(dashboard)/u/[username]/_components/sidebar/index.tsx b/app/(dashboard)/u/[username]/_components/sidebar/index.tsx new file mode 100644 index 0000000..ad294b2 --- /dev/null +++ b/app/(dashboard)/u/[username]/_components/sidebar/index.tsx @@ -0,0 +1,12 @@ +import { Navigation } from "./navigation"; +import { Toggle } from "./toggle"; +import { Wrapper } from "./wrapper"; + +export const Sidebar = async () => { + return ( + + + + + ); +}; diff --git a/app/(dashboard)/u/[username]/_components/sidebar/nav-item.tsx b/app/(dashboard)/u/[username]/_components/sidebar/nav-item.tsx new file mode 100644 index 0000000..b1a6991 --- /dev/null +++ b/app/(dashboard)/u/[username]/_components/sidebar/nav-item.tsx @@ -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 ( + + ); +}; + +export const NavItemSkeleton = () => { + return ( +
  • + +
    + +
    +
  • + ); +}; diff --git a/app/(dashboard)/u/[username]/_components/sidebar/navigation.tsx b/app/(dashboard)/u/[username]/_components/sidebar/navigation.tsx new file mode 100644 index 0000000..2d8539c --- /dev/null +++ b/app/(dashboard)/u/[username]/_components/sidebar/navigation.tsx @@ -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 ( +
    + {routes.map((route) => ( + + ))} +
    + ); +}; diff --git a/app/(dashboard)/u/[username]/_components/sidebar/toggle.tsx b/app/(dashboard)/u/[username]/_components/sidebar/toggle.tsx new file mode 100644 index 0000000..1802937 --- /dev/null +++ b/app/(dashboard)/u/[username]/_components/sidebar/toggle.tsx @@ -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 && ( +
    + + + +
    + )} + {!collapsed && ( +
    +

    Dashboard

    + + + +
    + )} + + ); +}; diff --git a/app/(dashboard)/u/[username]/_components/sidebar/wrapper.tsx b/app/(dashboard)/u/[username]/_components/sidebar/wrapper.tsx new file mode 100644 index 0000000..9c4ffd8 --- /dev/null +++ b/app/(dashboard)/u/[username]/_components/sidebar/wrapper.tsx @@ -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 ( + + ); +}; diff --git a/app/(dashboard)/u/[username]/layout.tsx b/app/(dashboard)/u/[username]/layout.tsx new file mode 100644 index 0000000..cbb24b7 --- /dev/null +++ b/app/(dashboard)/u/[username]/layout.tsx @@ -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 ( + <> + +
    + + {children} +
    + + ); +}; + +export default CreatorLayout; diff --git a/lib/auth-service.ts b/lib/auth-service.ts index 9682659..6dcfca1 100644 --- a/lib/auth-service.ts +++ b/lib/auth-service.ts @@ -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; }; diff --git a/renovate.json b/renovate.json deleted file mode 100644 index 5db72dd..0000000 --- a/renovate.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:recommended" - ] -} diff --git a/store/use-creator-sidebar.ts b/store/use-creator-sidebar.ts new file mode 100644 index 0000000..6b13b39 --- /dev/null +++ b/store/use-creator-sidebar.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +interface CreatorSidebarStore { + collapsed: boolean; + onExpand: () => void; + onCollapse: () => void; +} + +export const useCreatorSidebar = create((set) => ({ + collapsed: false, + onExpand: () => set(() => ({ collapsed: false })), + onCollapse: () => set(() => ({ collapsed: true })), +}));