Skip to main content

Overview

The application uses Next.js App Router’s layout system to create a consistent structure across pages. Layouts are composed of responsive grid systems, scrollable containers, and semantic HTML elements.

Root Layout

The root layout is defined in src/app/layout.tsx and wraps all pages in the application.
src/app/layout.tsx
import type { Metadata } from "next";
import { Archivo, Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Toaster } from "@/components/ui/sonner"

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const archivo = Archivo({
  variable: "--font-archivo",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Last Ping || Sheduling Messages",
  description: "Still Alive...",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} ${archivo.variable} antialiased`}
      >
        <Toaster />
        {children}
      </body>
    </html>
  );
}

Root Layout Features

1

Font Configuration

Loads Google Fonts (Geist Sans, Geist Mono, Archivo) as CSS variables
const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});
2

Metadata

Defines SEO metadata for the application
export const metadata: Metadata = {
  title: "Last Ping || Sheduling Messages",
  description: "Still Alive...",
};
3

Global Toaster

Includes the Sonner toast notification system
<Toaster />
4

Font Variables

Applies font CSS variables to the body element
className={`${geistSans.variable} ${geistMono.variable} ${archivo.variable} antialiased`}
The antialiased class applies font smoothing for better text rendering across browsers.

Main Page Layout

The main page layout demonstrates responsive design and component composition.
src/app/page.tsx
import Messages from "@/components/Messages";
import People from "@/components/People";
import Rescheduler from "@/components/Rescheduler";
import { Badge } from "@/components/ui/badge";
import { Clock } from "lucide-react";

export const dynamic = 'force-dynamic';

export default async function page() {
  if (!process.env.BACKEND_URL) {
    return <p>Backend URL not found</p>;
  }
  
  let data;
  try {
    const response = await fetch(`${process.env.BACKEND_URL}/time-left`, {
      method: "GET",
    });
    data = await response.json();
    if (response.status != 200) {
      <div className="h-screen grid place-items-center">
        <h1 className="font-bold text-4xl">Backend Cannot Be Accessed</h1>
      </div>;
    }
  } catch (e) {
    console.error(e);
    return (
      <div className="h-screen grid place-items-center">
        <h1 className="font-bold text-4xl">Backend Cannot Be Accessed</h1>
      </div>
    );
  }
  
  return (
    <div className="container p-5 mx-auto max-w-5xl">
      <div className="flex justify-center items-center pb-3 border-b-2 border-dashed gap-3 text-primary">
        <Badge variant="default" className="text-sm font-bold p-2 px-2">
          <Clock className="size-4" /> {data.timeLeft} h
        </Badge>
        <h1 className="text-lg font-bold">Message Scheduler</h1>
        <Rescheduler />
      </div>
      <div className="grid grid-cols-1 md:grid-cols-3 gap-2 pt-2">
        <Messages />
        <People />
      </div>
    </div>
  );
}

Page Layout Structure

<div className="container p-5 mx-auto max-w-5xl">
  {/* Page content */}
</div>
Classes explained:
  • container - Responsive container with padding
  • p-5 - Padding on all sides
  • mx-auto - Horizontal centering
  • max-w-5xl - Maximum width constraint (896px)

Responsive Design

The application uses Tailwind’s responsive breakpoints for mobile-first design.

Breakpoint System

sm: 640px   /* @media (min-width: 640px) */
md: 768px   /* @media (min-width: 768px) */
lg: 1024px  /* @media (min-width: 1024px) */
xl: 1280px  /* @media (min-width: 1280px) */
2xl: 1536px /* @media (min-width: 1536px) */

Responsive Patterns

Mobile (< 768px)

  • Single column layout
  • Messages above People
  • Full width components

Desktop (≥ 768px)

  • 3-column grid layout
  • Messages: 2 columns (md:col-span-2)
  • People: 1 column
  • Side-by-side view

Component Layout Patterns

Messages Component Layout

src/components/Messages.tsx
export default async function Messages() {
  const data = await fetch(`${process.env.BACKEND_URL}/messages/all`, {
    headers: {
      Authorization: `Basic ${Buffer.from(
        `${process.env.USERNAME}:${process.env.PASSWORD}`
      ).toString("base64")}`,
    },
  });
  if (!data.ok) {
    notFound();
  }
  const messages = await data.json();
  
  return (
    <div className="md:col-span-2">
      <h2 className="text-xl font-bold mb-2">Messages : </h2>
      <ScrollArea className="max-h-[calc(100vh-180px)] overflow-y-auto pr-4 mb-3">
        {messages.map((message: Message) => (
          <Message key={message.id} message={message} people={people} />
        ))}
      </ScrollArea>
      <Dialog>
        <DialogTrigger asChild>
          <Button className="w-full cursor-pointer flex justify-center items-center">
            <MailPlus />
            <span>Add Message</span>
          </Button>
        </DialogTrigger>
        <DialogContent>
          <DialogTitle className="text-lg font-semibold text-center">
            Add Message
          </DialogTitle>
          <CreateMessageForm people={people} />
        </DialogContent>
      </Dialog>
    </div>
  );
}
Layout Features:
1

Column Span

md:col-span-2 makes Messages take 2/3 of the grid on desktop
2

Section Header

text-xl font-bold mb-2 for the “Messages :” title
3

Scrollable Area

max-h-[calc(100vh-180px)] ensures list fits in viewport
4

Full Width Button

w-full makes the “Add Message” button span the container

People Component Layout

src/components/People.tsx
export default async function People() {
  const data = await fetch(`${process.env.BACKEND_URL}/people/all`, {
    headers: {
      Authorization: `Basic ${Buffer.from(
        `${process.env.USERNAME}:${process.env.PASSWORD}`
      ).toString("base64")}`,
    },
  });
  if (!data.ok) {
    notFound();
  }
  const people: People[] = await data.json();
  
  return (
    <div className="cursor-pointer mt-6 md:mt-0">
      <h2 className="text-xl font-bold mb-2">People : </h2>
      <ScrollArea className="max-h-[calc(100vh-180px)] overflow-y-auto pr-4 mb-3">
        {people.map((person: People) => (
          <Person key={person.id} person={person} />
        ))}
      </ScrollArea>
      <Dialog>
        <DialogTrigger asChild>
          <Button className="w-full cursor-pointer flex items-center justify-center">
            <UserPlus />
            <span>Add Person</span>
          </Button>
        </DialogTrigger>
        <DialogContent>
          <DialogTitle className="text-lg font-semibold text-center">
            Add Person
          </DialogTitle>
          <PersonCreate />
        </DialogContent>
      </Dialog>
    </div>
  );
}
Layout Features:
  • mt-6 md:mt-0 - Top margin on mobile (spacing from Messages), removed on desktop
  • Takes 1/3 of grid width on desktop (default, no col-span needed)
  • Same ScrollArea pattern as Messages

ScrollArea Pattern

Both Messages and People use ScrollArea for long lists:
<ScrollArea className="max-h-[calc(100vh-180px)] overflow-y-auto pr-4 mb-3">
  {items.map((item) => (
    <Item key={item.id} item={item} />
  ))}
</ScrollArea>
Classes explained:
  • max-h-[calc(100vh-180px)] - Maximum height based on viewport
    • 100vh - Full viewport height
    • -180px - Subtract space for header, footer, padding
  • overflow-y-auto - Vertical scrolling when content overflows
  • pr-4 - Right padding to prevent scrollbar overlap
  • mb-3 - Bottom margin for spacing before button
The calc() function ensures the scrollable area adapts to different screen heights while preventing page-level scrolling.

Card Layout Pattern

Individual items use the Card component with consistent spacing:
<Card className="border-1 border-dashed drop-shadow-xl gap-0 mb-2 cursor-pointer">
  <CardHeader className="font-bold">
    {message.content.slice(0, 100)} ...
  </CardHeader>
  <CardContent className="space-y-2">
    <CardDescription>
      Send to: {message.sendTo.name} ({message.sendToPhone})
    </CardDescription>
    <CardDescription>
      Send after: {message.sendAfter} Days
    </CardDescription>
  </CardContent>
</Card>
Styling breakdown:
  • border-1 border-dashed - Dashed border style
  • drop-shadow-xl - Large drop shadow for depth
  • gap-0 - No gap between card sections
  • mb-2 - Bottom margin between cards
  • cursor-pointer - Pointer cursor on hover
  • space-y-2 - Vertical spacing between descriptions

Error State Layout

The application shows error states with centered layouts:
<div className="h-screen grid place-items-center">
  <h1 className="font-bold text-4xl">Backend Cannot Be Accessed</h1>
</div>
Classes explained:
  • h-screen - Full viewport height
  • grid place-items-center - Center content both horizontally and vertically
  • font-bold text-4xl - Large, bold text for visibility

Dialog Layout

Dialogs are centered and responsive:
src/components/ui/dialog.tsx
function DialogContent({ className, children, ...props }) {
  return (
    <DialogPortal>
      <DialogOverlay />
      <DialogPrimitive.Content
        className={cn(
          "fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg",
          className
        )}
        {...props}
      >
        {children}
        <DialogPrimitive.Close className="absolute top-4 right-4">
          <XIcon />
        </DialogPrimitive.Close>
      </DialogPrimitive.Content>
    </DialogPortal>
  )
}
Positioning:
  • fixed top-[50%] left-[50%] - Fixed position at center
  • translate-x-[-50%] translate-y-[-50%] - Translate back by 50% to truly center
  • z-50 - High z-index to appear above other content
  • max-w-lg - Maximum width constraint (512px)
  • w-full - Full width up to max-w-lg
  • max-w-[calc(100%-2rem)] - Prevents dialog from touching screen edges on mobile

Spacing System

The application uses Tailwind’s spacing scale consistently:
0:   0px
1:   0.25rem (4px)
2:   0.5rem  (8px)
3:   0.75rem (12px)
4:   1rem    (16px)
5:   1.25rem (20px)
6:   1.5rem  (24px)

Layout Best Practices

Mobile First

Start with mobile layout, add responsive classes for larger screens

Consistent Spacing

Use Tailwind’s spacing scale for predictable layouts

Semantic HTML

Use proper HTML elements (header, main, section, etc.)

Accessibility

Ensure keyboard navigation and screen reader support

Performance Considerations

Server Components by Default

All layout components are Server Components unless they need interactivity:
// Server Component (default) - fetches data on server
export default async function Messages() {
  const data = await fetch(...);
  // ...
}
Server Components reduce JavaScript bundle size and improve initial page load performance.

Force Dynamic Rendering

The page uses export const dynamic = 'force-dynamic' to prevent caching:
src/app/page.tsx
export const dynamic = 'force-dynamic';
This ensures fresh data on every request, which is important for a messaging application.

Next Steps

Components Overview

Learn about the component architecture

Responsive Design

Deep dive into responsive patterns

Build docs developers (and LLMs) love