Skip to content

Commit

Permalink
move the dashboard route to app dir (#54)
Browse files Browse the repository at this point in the history
* make the dashboard route in app dir

* move dashboard route to app dir

* remove flaky test

* remove comment
  • Loading branch information
yesyash authored May 11, 2024
1 parent d5dba58 commit a190348
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 156 deletions.
24 changes: 24 additions & 0 deletions src/app/dashboard/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client"

import { useEffect } from "react"

export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error)
}, [error])

return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}
>
Try again
</button>
</div>
)
}
20 changes: 20 additions & 0 deletions src/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Metadata } from "next"

import { Navbar } from "@/components/navbar"

type Props = {
children: React.ReactNode
}

export const metadata: Metadata = {
title: "Dashboard",
}

export default function DashboardLayout({ children }: Props) {
return (
<div>
<Navbar />
<main className="ml-56 p-6">{children}</main>
</div>
)
}
3 changes: 3 additions & 0 deletions src/app/dashboard/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Loading() {
return <p>Loading...</p>
}
19 changes: 19 additions & 0 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { HydrationBoundary, QueryClient, dehydrate } from "@tanstack/react-query"

import { FormBuilderApi } from "@/api/form-builder/form-builder.api"
import { Dashboard } from "@/modules/dashboard"

export default async function DashboardPage() {
const queryClient = new QueryClient()

await queryClient.fetchQuery({
queryKey: ["FormBuilderApi.getAllForms"],
queryFn: FormBuilderApi.getAllForms,
})

return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Dashboard />
</HydrationBoundary>
)
}
22 changes: 22 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Inter } from "next/font/google"

import { cn } from "@/utils/classname"

import "@/styles/globals.css"
import Providers from "./providers"

export const inter = Inter({ subsets: ["latin"], variable: "--font-inter" })

type Props = {
children: React.ReactNode
}

export default function RootLayout({ children }: Props) {
return (
<html lang="en">
<body className={cn("font-sans", inter.className, inter.variable)}>
<Providers>{children}</Providers>
</body>
</html>
)
}
48 changes: 48 additions & 0 deletions src/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// In Next.js, this file would be called: app/providers.jsx
"use client"

// We can not useState or useRef in a server component, which is why we are
// extracting this part out into it's own file with 'use client' on top
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"

function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
})
}

let browserQueryClient: QueryClient | undefined = undefined

function getQueryClient() {
if (typeof window === "undefined") {
// Server: always make a new query client
return makeQueryClient()
} else {
// Browser: make a new query client if we don't already have one
// This is very important so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}

type ProviderProps = {
children: React.ReactNode
}

export default function Providers({ children }: ProviderProps) {
// NOTE: Avoid useState when initializing the query client if you don't
// have a suspense boundary between this and the code that may
// suspend because React will throw away the client on the initial
// render if it suspends and there is no boundary
const queryClient = getQueryClient()

return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
11 changes: 0 additions & 11 deletions src/components/navbar/__tests__/navbar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import "@testing-library/jest-dom"
import { render, screen } from "@testing-library/react"
import mockRouter from "next-router-mock"

import { Navbar } from ".."
import { NAVBAR_LINKS } from "../navbar.constants"
Expand All @@ -21,14 +20,4 @@ describe("Navbar", () => {
expect(links[index]).toHaveAttribute("href", link.href)
})
})

it("check if the active link is highlighted", () => {
const firstLink = NAVBAR_LINKS[0]
mockRouter.setCurrentUrl(firstLink.href)

render(<Navbar />)

const activeLink = screen.getByTestId(firstLink.title.toLowerCase())
expect(activeLink).toHaveAttribute("data-isactive", "true")
})
})
14 changes: 10 additions & 4 deletions src/components/navbar/navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use client"

import Link from "next/link"
import { useRouter } from "next/router"
import { usePathname } from "next/navigation"

import { cn } from "@/utils/classname"

Expand Down Expand Up @@ -29,17 +31,21 @@ const NavbarLink = ({ href, title, isActive }: NavbarLinkProps) => {
}

export const Navbar = () => {
const { pathname } = useRouter()
const pathname = usePathname()
const appName = process.env.NEXT_PUBLIC_APP_NAME

return (
<nav className="fixed bottom-2 left-0 w-full p-4 lg:relative lg:h-full lg:w-56 lg:border-r lg:border-stone-200 lg:px-2 xl:w-60">
<nav className="fixed bottom-2 left-0 w-full bg-white p-4 lg:h-full lg:w-56 lg:border-r lg:border-stone-200 lg:px-2 xl:w-60">
<h1 className="hidden px-2 py-4 text-2xl font-semibold text-stone-900 lg:block">{appName}</h1>

<ul className="mx-auto flex w-max justify-center gap-2 rounded-full border border-stone-100 bg-stone-100 p-2 shadow-lg shadow-stone-200 lg:w-full lg:flex-col lg:border-none lg:bg-transparent lg:shadow-none">
{NAVBAR_LINKS.map((link, index) => (
<li key={index} className="w-max lg:w-full">
<NavbarLink title={link.title} href={link.href} isActive={pathname.startsWith(link.href)} />
<NavbarLink
title={link.title}
href={link.href}
isActive={pathname?.startsWith(link.href) ?? false}
/>
</li>
))}
</ul>
Expand Down
26 changes: 0 additions & 26 deletions src/modules/common/tempaltes/dashboard-template.tsx

This file was deleted.

28 changes: 28 additions & 0 deletions src/modules/dashboard/components/forms-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client"

import { useQuery } from "@tanstack/react-query"

import { FormBuilderApi } from "@/api/form-builder/form-builder.api"

import { FormCard } from "./form-card"

export const FormsList = () => {
const { data } = useQuery({
queryKey: ["FormBuilderApi.getAllForms"],
queryFn: FormBuilderApi.getAllForms,
})

return (
<div>
{data?.data.map((form) => (
<FormCard
key={form.id}
id={form.id}
status={form.status}
title={form.content.blocks.find((block) => block.type === "FORM_TITLE")?.content ?? "Untitled Form"}
updated_at={form.updated_at}
/>
))}
</div>
)
}
118 changes: 6 additions & 112 deletions src/modules/dashboard/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,117 +1,11 @@
import { useQuery } from "@tanstack/react-query"
import { FilePlus, Plus, PlusIcon } from "lucide-react"
import Link from "next/link"

import { FormBuilderApi } from "@/api/form-builder/form-builder.api"
import { Button } from "@/components"
import { GenericError } from "@/components/generic-error/generic-error"
import { Shimmer } from "@/components/shimmer"

import { DashboardTemplate } from "../common/tempaltes/dashboard-template"

import { DashboardHeader, FormCard } from "./components"

const EmptyState = () => {
return (
<div className="flex flex-col items-center justify-center">
<div className="mb-4 w-max rounded-lg bg-stone-100 p-2 text-stone-700">
<FilePlus className="h-10 w-10" strokeWidth={1.5} />
</div>

<h3 className="pb-1 text-lg font-semibold text-stone-800">No forms yet</h3>
<p className="pb-5 text-center text-sm text-stone-500">Looks empty here, let&apos;s create a new form.</p>
<Button size="sm">
<PlusIcon />
Create form
</Button>
</div>
)
}
import { DashboardHeader } from "./components"
import { FormsList } from "./components/forms-list"

export const Dashboard = () => {
const { data, isLoading, isError } = useQuery({
queryKey: ["FormBuilderApi.getAllForms"],
queryFn: FormBuilderApi.getAllForms,
})

const appName = process.env.NEXT_PUBLIC_APP_NAME
const pageTitle = `Dashboard - ${appName}`

if (isLoading) {
return (
<DashboardTemplate title={pageTitle}>
<section className="w-full p-6">
<div className="flex items-center gap-2 pb-8">
<Shimmer className="h-8 w-24 lg:hidden" />
<Shimmer className="ml-auto h-8 w-full lg:h-9 lg:w-72" />
<Shimmer className="h-8 w-11 shrink-0 lg:h-9 lg:w-32" />
</div>

<div className="space-y-2">
{new Array(5).fill(0).map((_, index) => (
<Shimmer key={index} className="h-20" />
))}
</div>
</section>
</DashboardTemplate>
)
}

if (isError) {
return (
<DashboardTemplate title={pageTitle}>
<h1 className="p-6 text-xl font-semibold">{appName}</h1>
<section className="grid w-full flex-1 place-items-center p-6">
<GenericError />
</section>
</DashboardTemplate>
)
}

if (!data?.data.length) {
return (
<DashboardTemplate title={pageTitle}>
<h1 className="p-6 text-xl font-semibold">{appName}</h1>

<section className="grid w-full flex-1 place-items-center p-6">
<EmptyState />
</section>
</DashboardTemplate>
)
}

return (
<DashboardTemplate title={pageTitle}>
<section className="h-full w-full flex-1 overflow-auto bg-white px-6 pb-24">
<DashboardHeader className="pb-8 pt-4" />

<div className="space-y-2 pb-8 lg:space-y-1 lg:px-4 lg:pb-12">
{data?.data.map((form) => (
<FormCard
key={form.id}
id={form.id}
status={form.status}
title={
form.content.blocks.find((block) => block.type === "FORM_TITLE")?.content ??
"Untitled Form"
}
updated_at={form.updated_at}
/>
))}
</div>

<div className="-mx-4 lg:mx-0 lg:px-6">
<Button asChild variant="ghost" size="lg" className="px-4 text-2xl [&>svg]:h-7 [&>svg]:w-7">
<Link
href="/create"
className="flex w-max items-center gap-2 rounded-lg px-3 py-2 text-stone-300 transition hover:bg-stone-100 hover:text-stone-400"
>
<Plus />
<span className="font-medium">Create Form</span>
</Link>
</Button>
</div>
</section>
</DashboardTemplate>
<>
<DashboardHeader className="pb-8" />
<FormsList />
</>
)
}
3 changes: 0 additions & 3 deletions src/pages/dashboard.tsx

This file was deleted.

0 comments on commit a190348

Please sign in to comment.