Build a Simple Stock Photo Gallery App with NextJS & Unsplash API - Master NextJS Fundamentals!

 Welcome to our exciting journey of building a beautiful Stock Photo Gallery app using Next.js! If you're eager to expand your web development skills and delve into the world of modern front-end frameworks, you've come to the right place. In this comprehensive guide, we'll walk you through the entire process of creating a stunning photo gallery application step by step, while exploring the fundamentals of Next.js along the way.

Gone are the days of static websites; the web development landscape has evolved rapidly, and users now demand more dynamic and interactive experiences. That's where Next.js comes into play. As a powerful React framework, Next.js empowers developers to build server-rendered, static, and hybrid applications with ease. It seamlessly combines the best of both worlds – server-side rendering for optimal performance and client-side interactivity for a smooth user experience.


In this hands-on project, we'll leverage the magic of Next.js to construct a Stock Photo Gallery app that will mesmerize your visitors with its rich imagery and user-friendly interface. Whether you're a seasoned developer looking to upskill or a newcomer eager to embark on your coding journey, this guide is designed to cater to all levels of expertise.

Throughout the blog, we'll unveil the key concepts of Next.js, from creating dynamic pages and leveraging incremental static regeneration to handling data fetching on the server and client side. Our journey will take us through various features of Next.js, giving you a comprehensive understanding of how to harness its full potential.

Don't worry if you're not yet familiar with all the code snippets we've provided; we'll guide you through each line of code and explain its significance in building this incredible app. Additionally, we'll explore how to integrate popular libraries, such as React Bootstrap, to enhance the app's visual appeal and responsiveness.

By the end of this blog, you'll not only have a fully functional Stock Photo Gallery app at your disposal but also a solid grasp of Next.js fundamentals, enabling you to tackle even more ambitious projects in the future.

So, what are you waiting for? Grab your coding gear, buckle up, and let's embark on this exciting journey to create a dazzling Stock Photo Gallery app with Next.js! Let the learning and building begin!

Tutorial

First we will initiate a new app in our desired directory with the following command:

npx create-next-app@latest

We will choose Typescript, src directory, ESLint, default import alias for this project. We will reject the Tailwind installation and instead use react bootstrap library which can be installed with this command:

npm install react-bootstrap bootstrap

Now, within the src directory, we have an app directory with the layout.tsx that controls the global rendering of the root page instead of the index.tsx in previous versions.

Since, bootstrap components are client side rendered components and layout.tsx is server side rendered, we will first create a components folder in the src directory with a bootstrap.tsx file with the following exports:

"use client";

export { Container, SSRProvider, Alert, Spinner } from "react-bootstrap";

Next, we will import these bootstrap components in the layout.tsx to wrap them around our root component in the following way:

import "bootstrap/dist/css/bootstrap.min.css";
import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { Container, SSRProvider } from "@/components/bootstrap";
import NavBar from "./NavBar";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Stock Photo Gallery",
  description: "A stock photo gallery app built with NextJS. ",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <SSRProvider>
          <NavBar />
          <main>
            <Container className="py-4">{children}</Container>
          </main>
        </SSRProvider>
      </body>
    </html>
  );
}

The above code snippet that defines the layout for a web page. Let's break down the code step by step:

  1. Imports:
    • import "bootstrap/dist/css/bootstrap.min.css";: This line imports the Bootstrap CSS file, which is used to style the components in the application. Bootstrap is a popular CSS framework that provides pre-styled components and layout utilities.
    • import "./globals.css";: This line imports a custom CSS file called "globals.css." This file likely contains additional global styles that are used throughout the application.
  2. Type imports:
    • import type { Metadata } from "next";: This line imports the Metadata type from the Next.js library. The Metadata type is used to define metadata for the web page, such as its title and description.
  3. Font import:
    • import { Inter } from "next/font/google";: This line imports the Inter font from Google Fonts. The font is used in the application to style the text.
  4. Custom component imports:
    • import { Container, SSRProvider } from "@/components/bootstrap";: This line imports two custom components from the "@/components/bootstrap" module. These components are likely part of the application's component library and provide a Bootstrap-based container and SSRProvider.
  5. Font configuration:
    • const inter = Inter({ subsets: ["latin"] });: This line configures the Inter font by specifying that it should only include the "latin" subset. This means that only Latin characters will be loaded for the font, reducing the font file size and improving performance.
  6. Metadata definition:
    • export const metadata: Metadata = {...};: This code defines a constant variable named metadata that holds metadata information for the web page, including the title and description.
  7. RootLayout component:
    • export default function RootLayout({ children }: { children: React.ReactNode; }) { ... }: This is a functional component named RootLayout that serves as the layout for the web page. It takes a single prop called children, which represents the content that will be rendered within the layout.
  8. JSX content:
    • <html lang="en">: This line sets the language attribute of the HTML document to English.
    • <body className={inter.className}>: This line sets the class of the body element to the class name generated by the inter font, ensuring that the Inter font is applied to the whole page.
    • <SSRProvider>: This is a custom SSRProvider component, which likely wraps the entire page and provides server-side rendering functionality.
    • <NavBar />: This line renders the NavBar component. The NavBar component will be created later on for the top navigation.
    • <main>: This is the main content section of the page.
    • <Container className="py-4">{children}</Container>: This line renders the Container component with the Bootstrap class "py-4" (padding on the y-axis) and places the children prop inside it. The Container component likely provides a responsive container for the main content.

In summary, this Next.js code snippet imports various styles and components, configures a font, defines metadata for the page, and sets up a layout with a navigation bar and a responsive container for the main content. The layout is designed to use Bootstrap for styling, and the Inter font from Google Fonts is applied to the entire page.

After this we will delete the page.module.css file and delete everything from the page.tsx (we will add some text to this page in the end) make the following alterations to the global.css file:

:root {
  --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
    "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
    "Fira Mono", "Droid Sans Mono", "Courier New", monospace;

  --foreground-rgb: 0, 0, 0;
  --background-start-rgb: 214, 219, 220;
  --background-end-rgb: 255, 255, 255;
}

* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

html,
body {
  max-width: 100vw;
  min-height: 100vh;
}

body {
  color: rgb(var(--foreground-rgb));
  background: linear-gradient(
      to bottom,
      transparent,
      rgb(var(--background-end-rgb))
    )
    rgb(var(--background-start-rgb));
}

a {
  text-decoration: none;
}

img {
  background-color: lightblue;
}

Building the top navigation component

Now, we will build the NavBar.tsx component in the following way:

"use client";

import Link from "next/link";
import { Navbar, Nav, Container, NavDropdown } from "react-bootstrap";
import { usePathname } from "next/navigation";

export default function NavBar() {
  const pathname = usePathname();
  return (
    <Navbar
      bg="primary"
      variant="dark"
      sticky="top"
      expand="sm"
      collapseOnSelect
    >
      <Container>
        <Navbar.Brand as={Link} href="/">
          Stock Photo Gallery
        </Navbar.Brand>
        <Navbar.Toggle aria-controls="main-navbar" />
        <Navbar.Collapse id="main-navbar">
          <Nav>
            <Nav.Link as={Link} href="/static" active={pathname === "/static"}>
              Static
            </Nav.Link>
          </Nav>
        </Navbar.Collapse>
      </Container>
    </Navbar>
  );
}

The above code is a custom NavBar component. Let's break it down step by step:

  1. use client: The directive "use client" indicates NextJS that this component will be rendered on client side.
  2. Import statements:
    • import Link from "next/link";: This imports the Link component from Next.js, which is used for client-side navigation. The Link component allows you to create anchor tags (<a>) with client-side routing, meaning that when users click on the links, the page won't do a full reload but instead use client-side rendering to load the linked page.
    • import { Navbar, Nav, Container, NavDropdown } from "react-bootstrap";: This imports several components from the react-bootstrap library, which is a popular library for building responsive, mobile-first websites using Bootstrap styles and components. These components are used to create the responsive navigation bar.
    • import { usePathname } from "next/navigation";: This imports the usePathname hook from Next.js's next/navigation module. This hook allows accessing the current URL pathname. It is used to determine the active state of the navigation links based on the current URL.
  3. NavBar component:
    • export default function NavBar() { ... }: This is a functional component named NavBar, which represents the custom navigation bar for the application.
  4. usePathname hook:
    • const pathname = usePathname();: This line uses the usePathname hook to get the current URL pathname. The pathname variable will store the current path, which will be used to determine the active state of the navigation link.
  5. Navbar component:
    • <Navbar bg="primary" variant="dark" sticky="top" expand="sm" collapseOnSelect>: This creates a Bootstrap Navbar component with the following props:
      • bg="primary": Sets the background color of the Navbar to Bootstrap's primary color.
      • variant="dark": Sets the text color of the Navbar to dark.
      • sticky="top": Makes the Navbar stick to the top of the page when scrolling.
      • expand="sm": Specifies that the Navbar should collapse into a mobile-friendly version for small screens.
      • collapseOnSelect: Specifies that the Navbar should automatically close when a navigation link is selected on mobile devices.
  6. Container component:
    • <Container>: This is a Bootstrap Container component, which provides a responsive container for the Navbar content.
  7. Navbar.Brand component:
    • <Navbar.Brand as={Link} href="/">Stock Photo Gallery</Navbar.Brand>: This is the brand/logo section of the Navbar. It is wrapped in a Link component from Next.js, which allows navigating to the homepage ("/") without a full page refresh when clicked.
  8. Navbar.Toggle and Navbar.Collapse components:
    • <Navbar.Toggle aria-controls="main-navbar" />: This is the button that toggles the Navbar's collapsed state on small screens.
    • <Navbar.Collapse id="main-navbar">: This is the content section that collapses and expands based on the screen size.
  9. Nav component:
    • <Nav>: This is a Bootstrap Nav component, which contains the navigation links.
  10. Nav.Link component:
  • <Nav.Link as={Link} href="/static" active={pathname === "/static"}>Static</Nav.Link>: This is a navigation link. It is wrapped in a Link component and has an active prop that checks if the current pathname matches the link's href. If the pathname matches the href, the link is styled as active. The link's destination is set to "/static".

In summary, this code creates a responsive navigation bar using Bootstrap's Navbar, Nav, and Nav.Link components. It uses the Next.js Link component for client-side navigation, and the usePathname hook to determine the active state of the navigation link based on the current URL pathname. The navigation bar consists of a brand/logo section and a single navigation link called "Static."

Building Static Photo Page

For this we will create a (SSR) directory within the app folder. The parenthesis are used to remove the folder from allowing NextJS to take it up as a URL path name and instead just remain there for folder structuring. NextJS uses a file based routing so every folder created is a URL path name.

Before fetching data, in Typescript, we need to create an interface. We will create a models folder in the src directory with the file unsplash-image.ts with the following code:

export interface UnsplashImage {
  description: string;
  user: {
    username: string;
  };
  urls: {
    raw: string;
  };
  width: number;
  height: number;
}

export interface UnsplashImageSearchResponse {
  results: UnsplashImage[];
}

Within the (SSR) folder, we will create a static folder so that it can be accessed via localhost:3000/static and within it, we will create a page.tsx file with the following code:

import { UnsplashImage } from "@/models/unsplash-image";
import Image from "next/image";
import Link from "next/link";
import { Alert } from "@/components/bootstrap";
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Static Photo Page",
};

export default async function Page() {
  const response = await fetch(
    "<https://api.unsplash.com/photos/random?client_id=>" +
      process.env.UNSPLASH_ACCESS_KEY
  );
  const image: UnsplashImage = await response.json();
  const width = Math.min(500, image.width);
  const height = (width / image.width) * image.height;

  return (
    <div className="d-flex flex-column align-items-center">
      <Alert>
        This page <strong>fetches and caches data at build time.</strong> Even
        though Unsplash API always returns a new image, we see the same image
        after refreshing the page until we compile the project again.
      </Alert>
      <Image
        src={image.urls.raw}
        alt={image.description}
        width={width}
        height={height}
        className="rounded shadow mw-100 h-100"
      />
      by{" "}
      <Link href={"/users/" + image.user.username}>{image.user.username}</Link>
    </div>
  );
}

This is a Next.js page component that displays a static photo page. Let's break down the code step by step:

  1. Import statements:
    • import { UnsplashImage } from "@/models/unsplash-image";: This imports the UnsplashImage interface or type from the "@/models/unsplash-image" module. This interface likely represents the structure of an image object fetched from the Unsplash API.
    • import Image from "next/image";: This imports the Image component from Next.js. The Image component is used for optimizing images in the application by providing automatic responsive images with lazy loading and image optimization based on the device.
    • import Link from "next/link";: This imports the Link component from Next.js, which is used for client-side navigation.
    • import { Alert } from "@/components/bootstrap";: This imports the Alert component from a custom "@/components/bootstrap" module. The Alert component is likely a custom component that provides an alert box with Bootstrap styling.
    • import type { Metadata } from "next";: This imports the Metadata type from Next.js, which is used to define metadata for the page, such as its title and other properties.
  2. Metadata definition:
    • export const metadata: Metadata = {...};: This code defines a constant variable named metadata that holds metadata information for the web page. In this case, the page title is set to "Static Photo Page."
  3. Page component:
    • export default async function Page() { ... }: This is an asynchronous function component named Page, which is the main component that will be displayed for the static photo page.
  4. Fetching Unsplash image data:
    • const response = await fetch("<https://api.unsplash.com/photos/random?client_id=>" + process.env.UNSPLASH_ACCESS_KEY);: This line uses the fetch function to make a request to the Unsplash API and fetches a random photo. The API endpoint used is "https://api.unsplash.com/photos/random," and the request includes the client ID as a query parameter provided through the process.env.UNSPLASH_ACCESS_KEY. The response will contain information about the randomly fetched image.
    • const image: UnsplashImage = await response.json();: This line parses the JSON response obtained from the Unsplash API and assigns it to the image variable. The UnsplashImage type or interface (imported earlier) is used to type the image variable, ensuring that it matches the expected structure.
    • const width = Math.min(500, image.width);: This line calculates the width of the image to be displayed on the page. It takes the minimum value between 500 and the original width of the fetched image to ensure that the image doesn't exceed 500 pixels in width.
    • const height = (width / image.width) * image.height;: This line calculates the height of the image proportionally based on the adjusted width. It maintains the image's original aspect ratio.
  5. JSX content:
    • The returned JSX content is wrapped in a div element with the class "d-flex flex-column align-items-center." This uses Bootstrap's flexbox classes to center-align the page content.
    • <Alert>: This displays an alert box with a message about how the page fetches and caches data at build time. It explains that even though the Unsplash API always returns a new image, the same image is seen after refreshing the page until the project is compiled again.
    • <Image>: This renders the fetched image using the Next.js Image component. It sets the src attribute to image.urls.raw, which is the URL of the image provided by the Unsplash API. The alt attribute is set to image.description, which likely represents a brief description of the image. The width and height attributes are set to the calculated values, ensuring that the image is responsive and proportionally sized. The className attribute applies Bootstrap classes for styling the image as rounded, with a shadow, and taking the full available width.
    • <Link>: This creates a link to the user profile page of the photographer who uploaded the image. The href attribute sets the destination URL, which includes the user's username. The displayed content of the link is the username obtained from image.user.username.

In summary, this Next.js page component fetches a random image from the Unsplash API, displays it on the static photo page using the Next.js Image component for optimized rendering, and includes an alert explaining the behavior of data fetching and caching at build time. The page also provides a link to the user profile of the photographer who uploaded the image using the Link component for client-side navigation.

You can get your UNSPLASH key from Unsplash Image API | Free HD Photo API and store it in a new ‘.env.local’ file in the following way:

UNSPLASH_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

This will be something like this:

Figure 1: Static Page


Building the dynamic image on refresh

Next, we will create a dynamic folder within the (SSR) directory and within it we will create a page.tsx file with the following code:

import { UnsplashImage } from "@/models/unsplash-image";
import Image from "next/image";
import Link from "next/link";
import { Alert } from "@/components/bootstrap";
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Dynamic Photo Page",
};

export const revalidate = 0;

export default async function Page() {
  const response = await fetch(
    "<https://api.unsplash.com/photos/random?client_id=>" +
      process.env.UNSPLASH_ACCESS_KEY,
    {
      // cache: "no-cache",
      // cache: "no-store",
      //   next: {
      //     revalidate: 0,
      //   },
    }
  );
  const image: UnsplashImage = await response.json();
  const width = Math.min(500, image.width);
  const height = (width / image.width) * image.height;

  return (
    <div className="d-flex flex-column align-items-center">
      <Alert>
        This page <strong>fetches data dynamically.</strong> Every time we
        refresh the page, we see a new image. However, if we navigate to another
        page and then come back, we see the same image again.
      </Alert>
      <Image
        src={image.urls.raw}
        alt={image.description}
        width={width}
        height={height}
        className="rounded shadow mw-100 h-100"
      />
      by{" "}
      <Link href={"/users/" + image.user.username}>{image.user.username}</Link>
    </div>
  );
}

This is a Next.js page component that displays a dynamic photo page. Let's break down the code step by step:

  1. Import statements:
    • import { UnsplashImage } from "@/models/unsplash-image";: This imports the UnsplashImage interface or type from the "@/models/unsplash-image" module. This interface likely represents the structure of an image object fetched from the Unsplash API.
    • import Image from "next/image";: This imports the Image component from Next.js. The Image component is used for optimizing images in the application by providing automatic responsive images with lazy loading and image optimization based on the device.
    • import Link from "next/link";: This imports the Link component from Next.js, which is used for client-side navigation.
    • import { Alert } from "@/components/bootstrap";: This imports the Alert component from a custom "@/components/bootstrap" module. The Alert component is likely a custom component that provides an alert box with Bootstrap styling.
    • import type { Metadata } from "next";: This imports the Metadata type from Next.js, which is used to define metadata for the page, such as its title and other properties.
  2. Metadata definition:
    • export const metadata: Metadata = {...};: This code defines a constant variable named metadata that holds metadata information for the web page. In this case, the page title is set to "Dynamic Photo Page."
  3. revalidate variable:
    • export const revalidate = 0;: This code defines a constant variable named revalidate and sets it to 0. This variable is likely used to control the revalidation behavior of the page when using Incremental Static Regeneration (ISR) in Next.js. By setting it to 0, it disables revalidation, meaning that the page content won't be revalidated and updated on subsequent requests.
  4. Page component:
    • export default async function Page() { ... }: This is an asynchronous function component named Page, which is the main component that will be displayed for the dynamic photo page.
  5. Fetching Unsplash image data:
    • const response = await fetch("<https://api.unsplash.com/photos/random?client_id=>" + process.env.UNSPLASH_ACCESS_KEY, { ... });: This line uses the fetch function to make a request to the Unsplash API and fetches a random photo. The API endpoint used is "https://api.unsplash.com/photos/random," and the request includes the client ID as a query parameter provided through the process.env.UNSPLASH_ACCESS_KEY. The response will contain information about the randomly fetched image.
    • const image: UnsplashImage = await response.json();: This line parses the JSON response obtained from the Unsplash API and assigns it to the image variable. The UnsplashImage type or interface (imported earlier) is used to type the image variable, ensuring that it matches the expected structure.
    • const width = Math.min(500, image.width);: This line calculates the width of the image to be displayed on the page. It takes the minimum value between 500 and the original width of the fetched image to ensure that the image doesn't exceed 500 pixels in width.
    • const height = (width / image.width) * image.height;: This line calculates the height of the image proportionally based on the adjusted width. It maintains the image's original aspect ratio.
  6. JSX content:
    • The returned JSX content is wrapped in a div element with the class "d-flex flex-column align-items-center." This uses Bootstrap's flexbox classes to center-align the page content.
    • <Alert>: This displays an alert box with a message about how the page fetches data dynamically. It explains that every time the page is refreshed, a new image is seen. However, if the user navigates to another page and then comes back, the same image is seen again. This behavior is a result of how Next.js handles Incremental Static Regeneration.
    • <Image>: This renders the fetched image using the Next.js Image component. It sets the src attribute to image.urls.raw, which is the URL of the image provided by the Unsplash API. The alt attribute is set to image.description, which likely represents a brief description of the image. The width and height attributes are set to the calculated values, ensuring that the image is responsive and proportionally sized. The className attribute applies Bootstrap classes for styling the image as rounded, with a shadow, and taking the full available width.
    • <Link>: This creates a link to the user profile page of the photographer who uploaded the image. The href attribute sets the destination URL, which includes the user's username. The displayed content of the link is the username obtained from image.user.username.

In summary, this Next.js page component fetches a random image from the Unsplash API and displays it on the dynamic photo page using the Next.js Image component for optimized rendering. The page also provides an alert explaining the dynamic data fetching behavior. Additionally, there's a link to the user profile of the photographer who uploaded the image using the Link component for client-side navigation. The revalidate variable seems to disable revalidation, which means the page content won't be updated on subsequent requests using Incremental Static Regeneration.

Figure 2: Dynamic Image on refresh


Building the Incremental Static Regeneration Image

Next, we will create an ‘isr folder within the ‘(SSR)’ folder with the following page.tsx code:

import { UnsplashImage } from "@/models/unsplash-image";
import Image from "next/image";
import Link from "next/link";
import { Alert } from "@/components/bootstrap";
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Dynamic Photo Page",
};

export const revalidate = 15; // this means that the page will be revalidated every 15 seconds (if it is requested) and the new data will be used to render the page

export default async function Page() {
  const response = await fetch(
    "<https://api.unsplash.com/photos/random?client_id=>" +
      process.env.UNSPLASH_ACCESS_KEY,
    {
      // cache: "no-cache",
      // cache: "no-store",
      //   next: {
      //     revalidate: 0,
      //   },
    }
  );
  const image: UnsplashImage = await response.json();
  const width = Math.min(500, image.width);
  const height = (width / image.width) * image.height;

  return (
    <div className="d-flex flex-column align-items-center">
      <Alert>
        This page uses <strong>incremental static regeneration.</strong> A new
        image is fetched every 15 seconds (after refreshing the page) and then
        served from the cache for that duration.
      </Alert>
      <Image
        src={image.urls.raw}
        alt={image.description}
        width={width}
        height={height}
        className="rounded shadow mw-100 h-100"
      />
      by{" "}
      <Link href={"/users/" + image.user.username}>{image.user.username}</Link>
    </div>
  );
}

Let's break down the updated code:

  1. revalidate variable:
    • export const revalidate = 15;: This code defines a constant variable named revalidate and sets it to 15. This variable is used to control the revalidation behavior of the page when using Incremental Static Regeneration (ISR) in Next.js. Setting it to 15 means that the page will be revalidated every 15 seconds (if it is requested), and the new data will be used to render the page. This allows the dynamic photo page to periodically update with a new image every 15 seconds.
  2. Other aspects of the code remain the same, so let's summarize the entire code:

Summary: This Next.js page component displays a dynamic photo page that fetches a random image from the Unsplash API using client-side rendering (CSR) with Incremental Static Regeneration (ISR). Let's recap the key points:

  • Import statements: The necessary components and types/interfaces are imported.
  • Metadata definition: The metadata constant is defined to set the page title to "Dynamic Photo Page."
  • revalidate variable: The revalidate constant is defined to control the revalidation behavior of the page using ISR. Setting it to 15 means the page will revalidate every 15 seconds if it is requested.
  • Page component: The main component Page is defined as an asynchronous function.
  • Fetching Unsplash image data: The component fetches a random image from the Unsplash API using the fetch function. The image data is then extracted and used to calculate the appropriate dimensions for rendering the image on the page.
  • JSX content: The returned JSX content is wrapped in a div element with Bootstrap classes for alignment. It displays an Alert component indicating that the page uses Incremental Static Regeneration. It fetches a new image every 15 seconds and serves it from the cache for that duration. The Image component from Next.js is used to render the fetched image, and a Link component provides a link to the user profile of the photographer who uploaded the image.

In summary, this dynamic photo page uses Incremental Static Regeneration with a revalidation period of 15 seconds. The page will update and fetch a new image every 15 seconds, serving it from the cache for that duration. This allows the page to periodically display different images without requiring a full page refresh.

Figure 3: ISR Image regenerated after 15 sec


Also let’s not forget to add the above pages to the NavBar.tsx such that they are visible via the top nav bar in the following way:

"use client";

import Link from "next/link";
import { Navbar, Nav, Container, NavDropdown } from "react-bootstrap";
import { usePathname } from "next/navigation";

export default function NavBar() {
  const pathname = usePathname();
  return (
    <Navbar
      bg="primary"
      variant="dark"
      sticky="top"
      expand="sm"
      collapseOnSelect
    >
      <Container>
        <Navbar.Brand as={Link} href="/">
          Stock Photo Gallery
        </Navbar.Brand>
        <Navbar.Toggle aria-controls="main-navbar" />
        <Navbar.Collapse id="main-navbar">
          <Nav>
            <Nav.Link as={Link} href="/static" active={pathname === "/static"}>
              Static
            </Nav.Link>
            <Nav.Link
              as={Link}
              href="/dynamic"
              active={pathname === "/dynamic"}
            >
              Dynamic
            </Nav.Link>
            <Nav.Link as={Link} href="/isr" active={pathname === "/isr"}>
              ISR
            </Nav.Link>
          </Nav>
        </Navbar.Collapse>
      </Container>
    </Navbar>
  );
}

Building Dynamic Route Paths with Params

Now, we will create a topics folder within the (SSR) folder, within which we will create a [topic] folder (the square brackets make the path dynamic), and within this we will create a page.tsx file with the following code:

import { UnsplashImage } from "@/models/unsplash-image";
import Image from "next/image";
import styles from "./TopicPage.module.css";
import { Alert } from "@/components/bootstrap";
import { Metadata } from "next";

interface PageProps {
  params: { topic: string };
  // searchParams: { topic: string },
}

// make this dynamic
// export const revalidate = 0;

//tell nextjs to fetch some data during build time

export function generateMetadata({ params: { topic } }: PageProps): Metadata {
  return {
    title: topic + " - Stock Photo Gallery",
  };
}

export const generateStaticParams = async () => {
  // return [{ params: { topic: "nature" } }, { params: { topic: "cars" } }];
  return ["health", "cars", "nature"].map((topic) => ({ params: { topic } }));
};

//nextjs can also restrict user from some params
// export const dynamicParams = false;

export default async function Page({ params: { topic } }: PageProps) {
  const response = await fetch(
    `https://api.unsplash.com/photos/random?query=${topic}&count=10&client_id=${process.env.UNSPLASH_ACCESS_KEY}`
  );

  const images: UnsplashImage[] = await response.json();

  return (
    <div>
      <Alert>
        This page uses <strong>generateStaticParams</strong> to render and cache
        static pages at build time, even though the URL has a dynamic parameter.
        Pages that are not included in generateStaticParams will be fetched and
        rendered on first access and then cached for subsequent requests, which
        can be disabled.
      </Alert>
      <h1>{topic}</h1>
      {images.map((image) => (
        <Image
          key={image.urls.raw}
          src={image.urls.raw}
          alt={image.description}
          width={250}
          height={250}
          className={styles.image}
        />
      ))}
    </div>
  );
}

This is a Next.js page component for a topic page that displays a collection of images related to a specific topic. Let's break down the code step by step:

  1. Import statements:
    • import { UnsplashImage } from "@/models/unsplash-image";: This imports the UnsplashImage interface from "@/models/unsplash-image" module. This interface represents the structure of an image object fetched from the Unsplash API.
    • import Image from "next/image";: This imports the Image component from Next.js. The Image component is used for optimizing images in the application by providing automatic responsive images with lazy loading and image optimization based on the device.
    • import styles from "./TopicPage.module.css";: This imports a CSS module named "TopicPage.module.css" and assigns it to the styles object. CSS modules allow locally scoped styles for components to avoid global style conflicts.
    • import { Alert } from "@/components/bootstrap";: This imports the Alert component from a custom "@/components/bootstrap" module. The Alert component is likely a custom component that provides an alert box with Bootstrap styling.
    • import { Metadata } from "next";: This imports the Metadata type from Next.js. The Metadata type is used to define metadata for the page, such as its title.
  2. PageProps interface:
    • This interface is defined to specify the expected structure of the props that will be passed to the Page component. It has two properties:
      • params: { topic: string }: An object with a topic property of type string, representing the topic for the page.
  3. generateMetadata function:
    • This function is responsible for generating the metadata for the page based on the params object. It takes the params object and extracts the topic property to create the page's title. The title is set to be "{topic} - Stock Photo Gallery."
  4. generateStaticParams function:
    • This function is responsible for generating the static paths for the topic pages at build time. It returns an array of objects, each containing a params object with the topic property. The generateStaticParams function returns an array of topics (in this case, "health," "cars," and "nature") mapped to the appropriate params object.
  5. Page component:
    • The Page component is the main component responsible for rendering the topic page.
    • It receives the params object (with the topic property) as props.
    • It fetches a collection of images related to the specified topic from the Unsplash API using the fetch function.
    • The fetched images are stored in the images array.
    • The JSX content displays an Alert explaining how the static pages are generated using generateStaticParams. It also displays the topic as an h1 heading and then maps through the images array to render each image using the Image component from Next.js. The images are displayed with a fixed width and height of 250 pixels and styled using the CSS module TopicPage.module.css.

In summary, this Next.js page component renders a topic page that displays a collection of images related to the specified topic. The page is generated as a static page at build time using the generateStaticParams function, and the image data is fetched from the Unsplash API dynamically at runtime using the fetch function. The page also includes an Alert component and the topic as an h1 heading. The images are displayed using the Next.js Image component with fixed dimensions and styled using CSS modules.

We’ll also create a TopicPage.module.css file in the same [topic] directory with the following css:

.image {
  object-fit: cover;
  margin: 0.25rem;
  border-radius: 4px;
}

Let’s also add these topics in the NavBar.tsx:

"use client";

import Link from "next/link";
import { Navbar, Nav, Container, NavDropdown } from "react-bootstrap";
import { usePathname } from "next/navigation";

export default function NavBar() {
  const pathname = usePathname();
  return (
    <Navbar
      bg="primary"
      variant="dark"
      sticky="top"
      expand="sm"
      collapseOnSelect
    >
      <Container>
        <Navbar.Brand as={Link} href="/">
          Stock Photo Gallery
        </Navbar.Brand>
        <Navbar.Toggle aria-controls="main-navbar" />
        <Navbar.Collapse id="main-navbar">
          <Nav>
            <Nav.Link as={Link} href="/static" active={pathname === "/static"}>
              Static
            </Nav.Link>
            <Nav.Link
              as={Link}
              href="/dynamic"
              active={pathname === "/dynamic"}
            >
              Dynamic
            </Nav.Link>
            <Nav.Link as={Link} href="/isr" active={pathname === "/isr"}>
              ISR
            </Nav.Link>
    
            <NavDropdown title="Topics" id="topics-dropdown">
              <NavDropdown.Item
                as={Link}
                href="/topics/health"
                active={pathname === "/topics/[topic]"}
              >
                Health
              </NavDropdown.Item>
              <NavDropdown.Item
                as={Link}
                href="/topics/cars"
                active={pathname === "/topics/[topic]"}
              >
                Cars
              </NavDropdown.Item>
              <NavDropdown.Item
                as={Link}
                href="/topics/nature"
                active={pathname === "/topics/[topic]"}
              >
                Nature
              </NavDropdown.Item>
            </NavDropdown>
          </Nav>
        </Navbar.Collapse>
      </Container>
    </Navbar>
  );
}
Figure 4: Topic based routing and fetching data


Building Search Function to Show Client Side Fetching

For this, we will first create a (CSR) folder within the app directory, within which we will create a search folder where we will have a page.tsx with the following code:

import SearchPage from "./SearchPage";

import { Metadata } from "next";
import { Alert } from "@/components/bootstrap";

export const metadata: Metadata = {
  title: "Search - Stock Photo Gallery",
};

// here the server component becomes the wrapper for the client component which the SearchPage component
//you can also fetch data here and pass it to the client component as props

export default function Page() {
  return (
    <div>
      <Alert>
        {" "}
        This page fetches data <strong>client-side</strong>. In order to not
        leak API credentials, the request is sent to a NextJS{" "}
        <strong>route handler</strong> that runs on the server and then forwards
        the request to the Unsplash API. This route handler then fetches data
        from the API and returns it to the client.{" "}
      </Alert>
      <SearchPage />
    </div>
  );
}

This is a Next.js page component that serves as a wrapper for the SearchPage component. Let's break down the code step by step:

  1. Import statements:
    • import SearchPage from "./SearchPage";: This imports the SearchPage component from the file "SearchPage.js" (or "SearchPage.tsx"). The SearchPage component is likely a client-side component responsible for displaying a search page.
    • import { Metadata } from "next";: This imports the Metadata type from Next.js. The Metadata type is used to define metadata for the page, such as its title.
    • import { Alert } from "@/components/bootstrap";: This imports the Alert component from a custom "@/components/bootstrap" module. The Alert component is likely a custom component that provides an alert box with Bootstrap styling.
  2. Metadata definition:
    • export const metadata: Metadata = {...};: This code defines a constant variable named metadata that holds metadata information for the web page. In this case, the page title is set to "Search - Stock Photo Gallery."
  3. Page component:
    • export default function Page() { ... }: This is the default function component named Page, which is the main component that will be displayed for the page.
  4. JSX content:
    • The returned JSX content is wrapped in a div element.
    • It displays an Alert component explaining that the page fetches data client-side. To avoid leaking API credentials, the request is sent to a Next.js route handler that runs on the server. The route handler forwards the request to the Unsplash API, fetches the data, and returns it to the client. This approach helps protect sensitive API credentials from being exposed on the client-side.
    • The SearchPage component is rendered below the alert using the <SearchPage /> syntax. The SearchPage component is imported and used as a child component here. This component likely handles the client-side rendering and functionality for the search page.

In summary, this Next.js page component serves as a wrapper for the SearchPage component. It displays an alert explaining that data fetching occurs on the client-side, but the actual API request is forwarded through a server-side route handler to avoid leaking API credentials. The SearchPage component is then rendered below the alert to handle the client-side rendering and functionality for the search page.

After this, we will create a SearchPage.tsx file in the same directory and write the following logic:

"use client";

import { UnsplashImage } from "@/models/unsplash-image";
import { FormEvent, useState } from "react";
import { Button, Form, Spinner } from "react-bootstrap";
import Image from "next/image";
import styles from "./SearchPage.module.css";

export default function SearchPage() {
  const [searchResults, setSearchResults] = useState<UnsplashImage[] | null>(
    []
  );

  const [searchResultsLoading, setSearchResultsLoading] = useState(false);
  const [searchResultsLoadingIsError, setSearchResultsLoadingIsError] =
    useState(false);

  async function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();

    const formData = new FormData(e.target as HTMLFormElement);
    const query = formData.get("query")?.toString().trim();

    if (query) {
      try {
        setSearchResults(null);
        setSearchResultsLoading(true);
        setSearchResultsLoadingIsError(false);
        const response = await fetch(`/api/search?query=${query}`);
        const images: UnsplashImage[] = await response.json();
        setSearchResults(images);
      } catch (error) {
        setSearchResultsLoadingIsError(true);
      } finally {
        setSearchResultsLoading(false);
      }
    }
  }
  return (
    <div>
      <Form onSubmit={handleSubmit}>
        <Form.Group className="mb-3" controlId="search-input">
          <Form.Label>Search</Form.Label>
          <Form.Control
            name="query"
            placeholder="Eg. Nature, Cars, Health, etc."
          />
        </Form.Group>
        <Button type="submit" className="mb-3" disabled={searchResultsLoading}>
          Search
        </Button>
      </Form>

      <div className="d-flex flex-column align-items-center">
        {searchResultsLoading && <Spinner animation="border" />}
        {searchResultsLoadingIsError && (
          <p>Something went wrong. Please try again.</p>
        )}
        {searchResults?.length === 0 && (
          <p>Nothing found. Try a different query</p>
        )}
      </div>

      {searchResults && (
        <>
          {searchResults.map((image) => (
            <Image
              key={image.urls.raw}
              src={image.urls.raw}
              alt={image.description}
              width={250}
              height={250}
              className={styles.image}
            />
          ))}
        </>
      )}
    </div>
  );
}

This is a client-side component for a search page that allows users to search for images related to a specific query using the Unsplash API. Let's break down the code step by step:

  1. Import statements:
    • import { UnsplashImage } from "@/models/unsplash-image";: This imports the UnsplashImage interface from "@/models/unsplash-image" module. This interface represents the structure of an image object fetched from the Unsplash API.
    • import { FormEvent, useState } from "react";: This imports the FormEvent type and the useState hook from the React library. The FormEvent type is used to define the event object for form submissions, and the useState hook is used for managing state in functional components.
    • import { Button, Form, Spinner } from "react-bootstrap";: This imports several components from the React Bootstrap library, including Button, Form, and Spinner.
    • import Image from "next/image";: This imports the Image component from Next.js. The Image component is used for optimizing images in the application by providing automatic responsive images with lazy loading and image optimization based on the device.
    • import styles from "./SearchPage.module.css";: This imports a CSS module named "SearchPage.module.css" and assigns it to the styles object. CSS modules allow locally scoped styles for components to avoid global style conflicts.
  2. State variables:
    • const [searchResults, setSearchResults] = useState<UnsplashImage[] | null>([]);: This creates a state variable searchResults and a corresponding function setSearchResults using the useState hook. The initial value of searchResults is an empty array, and its type is specified as an array of UnsplashImage objects or null.
    • const [searchResultsLoading, setSearchResultsLoading] = useState(false);: This creates a state variable searchResultsLoading and a corresponding function setSearchResultsLoading. The initial value of searchResultsLoading is false, indicating that the search results are not currently loading.
    • const [searchResultsLoadingIsError, setSearchResultsLoadingIsError] = useState(false);: This creates a state variable searchResultsLoadingIsError and a corresponding function setSearchResultsLoadingIsError. The initial value of searchResultsLoadingIsError is false, indicating that there is no error with the search results loading.
  3. handleSubmit function:
    • This function is called when the user submits the search form.
    • It prevents the default form submission behavior using e.preventDefault().
    • It retrieves the query from the form input, trims any leading or trailing spaces, and stores it in the query variable.
    • If a valid query is provided, the function sets the state variables to handle the loading and error states. It sets searchResults to null to indicate that the search results are being loaded, sets searchResultsLoading to true to trigger the display of a loading spinner, and sets searchResultsLoadingIsError to false to hide any previous error messages.
    • The function then makes a request to the /api/search route on the server, passing the query as a parameter. It expects the server to handle the API request to the Unsplash API and return the search results.
    • If the request is successful, the function sets the searchResults state variable to the fetched images array.
    • If there's an error during the request, it sets searchResultsLoadingIsError to true to trigger the display of an error message.
    • Finally, regardless of the request's outcome, the function sets searchResultsLoading to false to hide the loading spinner.
  4. JSX content:
    • The returned JSX content starts with a Form element that allows users to input their search query. When the form is submitted, the handleSubmit function is called.
    • A loading spinner (<Spinner>) is displayed if searchResultsLoading is true.
    • An error message is displayed if searchResultsLoadingIsError is true.
    • If there are no search results (searchResults is an empty array), a message is displayed indicating that nothing was found for the search query.
    • If there are search results, the Image component from Next.js is used to display the images. Each image is mapped through the searchResults array, and its URL, description, width, and height are provided as props to the Image component. The images are styled using CSS modules with a fixed width and height of 250 pixels.

In summary, this client-side component is a search page that allows users to search for images related to a specific query using the Unsplash API. It displays a form for inputting the search query, and upon submission, it fetches the search results from the server. The component handles loading and error states while fetching the results. If there are search results, it displays the images with fixed dimensions. The search and image rendering occur on the client-side, providing an interactive and responsive search experience for the user.

We’ll also create a SearchPage.module.css file in the same directory with the following css:

.image {
  object-fit: cover;
  margin: 0.25rem;
  border-radius: 4px;
}

For this, we will also create an api folder within the app directory within which we’ll have a search folder, and within that we’ll put a route.tsx file with the following code:

import { UnsplashImageSearchResponse } from "@/models/unsplash-image";
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const query = searchParams.get("query");

  if (!query) {
    return NextResponse.json({ error: "No query provided" }, { status: 400 });
  }

  const response = await fetch(
    `https://api.unsplash.com/search/photos?query=${query}&client_id=${process.env.UNSPLASH_ACCESS_KEY}`
  );

  const { results }: UnsplashImageSearchResponse = await response.json();

  return NextResponse.json(results);
}

This code represents a server-side route handler function for handling GET requests to the /api/search endpoint in a Next.js application. Let's break down the code step by step:

  1. Import statements:
    • import { UnsplashImageSearchResponse } from "@/models/unsplash-image";: This imports the UnsplashImageSearchResponse interface from "@/models/unsplash-image" module. This interface represents the structure of a response from a search for Unsplash images.
    • import { NextResponse } from "next/server";: This imports the NextResponse object from Next.js server APIs. The NextResponse object is used to create custom responses for server routes.
  2. Route handler function:
    • export async function GET(request: Request) { ... }: This code exports an asynchronous function named GET that receives a request object as an argument. The request object represents the incoming HTTP request to the /api/search endpoint.
  3. Parsing the query parameter:
    • const { searchParams } = new URL(request.url);: This line creates a new URL object from the request.url and extracts the searchParams property. The searchParams object contains the query parameters from the URL.
    • const query = searchParams.get("query");: This line retrieves the value of the "query" parameter from the searchParams object. It will contain the user's search query provided in the URL.
  4. Handling invalid queries:
    • The code checks if a valid query exists. If not, it returns a custom JSON response using NextResponse.json. The response object contains an error message ("No query provided") and sets the HTTP status code to 400 (Bad Request). This indicates that the user did not provide a valid search query.
  5. Fetching search results from Unsplash API:
    • If a valid query exists, the code proceeds to make a request to the Unsplash API's search endpoint using fetch. It includes the user's search query in the request URL and passes the API access key from the environment variable.
    • The response from the Unsplash API is then parsed as JSON, and the results property is extracted from the response data. The results property contains an array of Unsplash images that match the search query.
  6. Returning the search results:
    • The code returns the search results as a JSON response using NextResponse.json. The results array is sent as the response body.

In summary, this server-side route handler is responsible for handling GET requests to the /api/search endpoint in a Next.js application. It takes the user's search query from the URL, fetches the corresponding images from the Unsplash API, and returns the search results as a JSON response. If no valid query is provided, it returns an error response with a 400 status code. This route handler acts as a proxy between the client and the Unsplash API, allowing the client-side component to access the Unsplash API data securely without exposing the API access key on the client-side.

Now, we will also add this Search page to our NavBar.tsx:

"use client";

import Link from "next/link";
import { Navbar, Nav, Container, NavDropdown } from "react-bootstrap";
import { usePathname } from "next/navigation";

export default function NavBar() {
  const pathname = usePathname();
  return (
    <Navbar
      bg="primary"
      variant="dark"
      sticky="top"
      expand="sm"
      collapseOnSelect
    >
      <Container>
        <Navbar.Brand as={Link} href="/">
          Stock Photo Gallery
        </Navbar.Brand>
        <Navbar.Toggle aria-controls="main-navbar" />
        <Navbar.Collapse id="main-navbar">
          <Nav>
            <Nav.Link as={Link} href="/static" active={pathname === "/static"}>
              Static
            </Nav.Link>
            <Nav.Link
              as={Link}
              href="/dynamic"
              active={pathname === "/dynamic"}
            >
              Dynamic
            </Nav.Link>
            <Nav.Link as={Link} href="/isr" active={pathname === "/isr"}>
              ISR
            </Nav.Link>
            <Nav.Link as={Link} href="/search" active={pathname === "/search"}>
              Search
            </Nav.Link>
            <NavDropdown title="Topics" id="topics-dropdown">
              <NavDropdown.Item
                as={Link}
                href="/topics/health"
                active={pathname === "/topics/[topic]"}
              >
                Health
              </NavDropdown.Item>
              <NavDropdown.Item
                as={Link}
                href="/topics/cars"
                active={pathname === "/topics/[topic]"}
              >
                Cars
              </NavDropdown.Item>
              <NavDropdown.Item
                as={Link}
                href="/topics/nature"
                active={pathname === "/topics/[topic]"}
              >
                Nature
              </NavDropdown.Item>
            </NavDropdown>
          </Nav>
        </Navbar.Collapse>
      </Container>
    </Navbar>
  );
}

This code defines a navigation bar component (NavBar) using the React Bootstrap library. It provides a responsive and collapsible navigation bar with links to different pages and dropdown menus for topics. Let's break down the code step by step:

  1. Import statements:
    • import Link from "next/link";: This imports the Link component from Next.js, which is used for client-side navigation between pages.
    • import { Navbar, Nav, Container, NavDropdown } from "react-bootstrap";: This imports several components from the React Bootstrap library, including Navbar, Nav, Container, and NavDropdown. These components are used to create a responsive navigation bar with dropdown menus.
    • import { usePathname } from "next/navigation";: This imports the usePathname hook from Next.js, which allows access to the current pathname of the URL. It will be used to determine the active link in the navigation bar.
  2. NavBar component:
    • The NavBar component is a functional component that returns the JSX content for the navigation bar.
  3. pathname variable:
    • const pathname = usePathname();: This line initializes a variable pathname using the usePathname hook. It holds the current pathname of the URL, which is used to determine the active link in the navigation bar.
  4. JSX content:
    • The returned JSX content starts with the Navbar component from React Bootstrap. It sets various props such as bg, variant, sticky, expand, and collapseOnSelect to define the appearance and behavior of the navigation bar.
    • Within the Navbar, there is a Container component, which acts as a wrapper to keep the content within a fixed-width container.
    • The navigation bar includes a brand link (Navbar.Brand) that displays "Stock Photo Gallery" as the brand text. The brand link takes users to the home page ("/") when clicked.
    • The navigation bar has a toggle button (Navbar.Toggle) to collapse the navigation links on smaller screens.
    • The navigation links are contained within a Navbar.Collapse component, which is hidden on smaller screens and toggled by the toggle button.
    • The Nav component contains multiple Nav.Link components, representing links to different pages. Each Nav.Link is created using the Link component from Next.js. The active prop of each Nav.Link is set to determine whether the link should be displayed as active based on the current pathname. The active prop is true if the pathname matches the link's href, and false otherwise.
    • There is also a NavDropdown component for the "Topics" dropdown menu. It contains several NavDropdown.Item components, each representing a link to a specific topic page. The NavDropdown.Item links are also created using the Link component from Next.js, and the active prop is used to determine whether a specific topic link is active based on the current pathname.
  5. Link components in NavDropdown.Item:
    • The NavDropdown.Item components for different topics use the same Link component, but they have different href props that lead to different topic pages ("/topics/health", "/topics/cars", and "/topics/nature"). The active prop of each NavDropdown.Item is set to true when the pathname matches "/topics/[topic]", where [topic] is a dynamic segment representing any topic name.

In summary, this NavBar component creates a responsive navigation bar with links to different pages and a dropdown menu for topics. The component uses the Link component from Next.js to handle client-side navigation between pages. The active prop is used to highlight the current active link based on the current pathname.

Figure 5: Search Page with Search Params


Building the User Info page

For this we will first create a model called unsplash-user.ts file in the models directory:

export interface UnsplashUser {
  username: string;
  first_name: string;
  last_name: string;
}

Now, within the previously created (SSR) folder, we will create a users folder, within which we will create a [users] folder, with a page.tsx file:

import { UnsplashUser } from "@/models/unsplash-user";
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { Alert } from "@/components/bootstrap";

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

async function getUser(username: string): Promise<UnsplashUser> {
  const response = await fetch(
    `https://api.unsplash.com/users/${username}?client_id=${process.env.UNSPLASH_ACCESS_KEY}`
  );
  if (response.status === 404) notFound();
  return await response.json();
}

export async function generateMetadata({
  params: { username },
}: PageProps): Promise<Metadata> {
  const user = await getUser(username);

  return {
    title:
      ([user.first_name, user.last_name].filter(Boolean).join(" ") ||
        user.username) + " - Stock Photo Gallery", //if first_name and last_name are not empty, join them with a space, otherwise use username
    // the filter is used to remove empty strings from the array before joining
  };
}

export default async function Page({ params: { username } }: PageProps) {
  const user = await getUser(username);
  return (
    <div>
      <Alert>
        This profile page uses <strong>generateMetadata</strong> to set the{" "}
        <strong>page title </strong> dynamically from the API response.
      </Alert>
      <h1>{user.username}</h1>
      <p>First Name: {user.first_name}</p>
      <p>First Name: {user.last_name}</p>
      <a href={"<https://unsplash.com/>" + user.username}>
        Unsplash User Profile
      </a>
    </div>
  );
}

This code represents a Next.js page component that serves as a dynamic profile page for displaying information about a user from the Unsplash API. The page dynamically fetches user data based on the username provided in the URL. Let's break down the code step by step:

  1. Import statements:
    • import { UnsplashUser } from "@/models/unsplash-user";: This imports the UnsplashUser interface from "@/models/unsplash-user" module. This interface represents the structure of a user object fetched from the Unsplash API.
    • import { Metadata } from "next";: This imports the Metadata type from Next.js. The Metadata type is used to define metadata for the page, such as its title.
    • import { notFound } from "next/navigation";: This imports the notFound function from Next.js navigation utilities. It is used to return a 404 response if a user is not found.
    • import { Alert } from "@/components/bootstrap";: This imports the Alert component from a custom "@/components/bootstrap" module. The Alert component is likely a custom component that provides an alert box with Bootstrap styling.
  2. PageProps interface:
    • interface PageProps { params: { username: string }; }: This code defines an interface named PageProps with a params property that has a username property of type string. This interface will be used to pass the dynamic parameters to the page component.
  3. getUser function:
    • async function getUser(username: string): Promise<UnsplashUser> { ... }: This code defines an asynchronous function named getUser that takes a username as a parameter and returns a Promise of type UnsplashUser. The function fetches user data from the Unsplash API based on the provided username.
    • Inside the function, a request is made to the Unsplash API endpoint for the specific user using fetch. The API response is then checked for a 404 status code, and if a user is not found, the notFound() function is called to return a 404 response.
  4. generateMetadata function:
    • export async function generateMetadata({ params: { username } }: PageProps): Promise<Metadata> { ... }: This code exports an asynchronous function named generateMetadata. It takes the username from the PageProps interface and returns a Promise of type Metadata. The function dynamically generates metadata for the page, including the title.
    • Inside the function, the getUser function is called to fetch user data based on the provided username.
    • The Metadata object is then constructed using the user's first name, last name, and username. If both first name and last name are not empty, they are joined with a space in the title. Otherwise, the username is used.
  5. Page component:
    • The Page component is a default function component that takes the username from the PageProps interface and returns the JSX content for the profile page.
    • The getUser function is called again inside the component to fetch user data based on the provided username.
    • The JSX content displays an Alert component explaining that the page uses generateMetadata to set the page title dynamically from the API response.
    • The user's username, first name, and last name are displayed in separate paragraphs (<p>).
    • A link to the user's profile on Unsplash is displayed using an anchor (<a>) tag with the user.username as the URL.

In summary, this Next.js page component serves as a dynamic profile page for displaying information about a user from the Unsplash API. The generateMetadata function dynamically generates the page title based on the user's data fetched from the API. The component fetches the user's data and displays it along with an alert explaining the dynamic page title generation. If a user is not found (404 response from the API), the notFound function is called to return a 404 response for the page.

Figure 6: User Profile


404, Error & Loading Pages

Next, we’ll create some basic pages for error handling. Please note that the file names for these files matters a lot because NextJS scans the app directory to look for these specific names.

First we’ll create a not-found.tsx file in the app directory with the following code:

export default function NotFound() {
  return (
    <div>
      <h1>404 - Page Not Found</h1>
    </div>
  );
}

Next, we will create a error.tsx file within the same directory with the following code:

"use client";

import { Button } from "react-bootstrap";

interface ErrorPageProps {
  error: Error;
  reset: () => void;
}

export default function ErrorPage({ error, reset }: ErrorPageProps) {
  return (
    <div className="text-center">
      <h1>Oops!</h1>
      <p className="lead">An error occurred.</p>
      <p className="text-muted">{error.message}</p>
      <Button variant="primary" onClick={reset}>
        Try again
      </Button>
    </div>
  );
}

Next, we will create a loading.tsx file in thapp directory with the following code:

import { Spinner } from "@/components/bootstrap";
export default function Loading() {
  return <Spinner animation="border" className="d-block m-auto" />;
}

Lastly, we will populate the page.tsx file in the app directory with the following text blocks:

import { Alert } from "@/components/bootstrap";

export default function Home() {
  return (
    <div>
      <Alert>
        <p>
          This is a sample project to showcase and learn the new{" "}
          <strong>NextJS 13 app directory</strong> and its features, including:
        </p>
        <ul>
          <li>static and dynamic server-side rendering</li>
          <li>incremental static regeneration</li>
          <li>client-side rendering</li>
          <li>route handlers (API endpoints)</li>
          <li>meta-data API</li>
          <li>and more</li>
        </ul>
        <p className="mb-0">
          Every page uses a different approach to{" "}
          <strong>fetching and caching data</strong>. Click the links in the nav
          bar to try them out.
        </p>
      </Alert>
      <Alert variant="secondary">
        <p>
          Note: In order to load the data on this site, you need to get a{" "}
          <a href="<https://unsplash.com/developers>">
            free API key from Unsplash
          </a>{" "}
          and add it to your <code>.env.local</code> file as{" "}
          <code>UNSPLASH_ACCESS_KEY</code>.
        </p>
        <p className="mb-0">
          Unsplash has a free quota of 50 requests per hour so you might start
          getting errors if you try too often.
        </p>
      </Alert>
    </div>
  );
}

I followed the YouTube tutorial by The Most Efficient Next.JS 13.4 Beginner Tutorial (TypeScript) - YouTube and you can checkout the video to get a walkthrough. I really enjoy building documentations of whatever project I work on, whether it is from an online tutorial or my personal projects. I feel like sometimes there are some people who prefer reading code instead of following along several hour long tutorials on YouTube. Also, such written tutorials provide people with specific information from within the article whenever they are looking for something specific instead of learning the whole framework from scratch.

You can checkout the Github Repo for this project at: gupta-karan1/stock-photos-app-nextjs (github.com) and even checkout the Vercel deployment at Stock Photo Gallery (stock-photos-app-nextjs.vercel.app) to go through each page.

Conclusion

Congratulations on completing this thrilling adventure of building a captivating Stock Photo Gallery app with Next.js! Throughout this journey, we've explored the core concepts of Next.js, unraveling its magic to create a seamless user experience that blends server-side rendering and client-side interactivity flawlessly.

We started by setting the foundation with an introduction to Next.js and its significance in modern web development. From there, we dived straight into the project, crafting dynamic pages, handling data fetching, and even implementing incremental static regeneration to optimize performance.

As you worked through the code and concepts, you might have encountered some challenges – and that's perfectly normal! Remember, every challenge is an opportunity to grow and enhance your skills. The journey of a developer is a continuous learning process, and building this app has undoubtedly given you valuable hands-on experience with Next.js.

The Stock Photo Gallery app you've crafted is not just an outcome; it's a testament to your dedication and passion for web development. It showcases your ability to think critically, solve problems, and bring your creative visions to life.

But our journey doesn't end here. Next.js is a powerful tool with endless possibilities, and you now have a solid foundation to explore its advanced features and tackle even more ambitious projects. Whether you're building e-commerce platforms, blogs, or complex web applications, Next.js will be your trusted ally in creating cutting-edge experiences for your users.

As you continue your development journey, don't forget to stay curious and keep exploring new technologies and frameworks. The web development world is constantly evolving, and there's always something new to learn and discover.

We hope you've enjoyed building the Stock Photo Gallery app as much as we enjoyed guiding you through it. Your journey as a developer is only just beginning, and the future holds infinite opportunities for growth and success.

Now go forth with your newfound knowledge and passion for Next.js, and let your creativity soar as you embark on exciting projects that will leave a mark on the digital landscape.

Thank you for joining us on this adventure, and remember – the sky's the limit for what you can achieve with Next.js in your toolkit. Happy coding!


Popular Posts

Perform CRUD (Create, Read, Update, Delete) Operations using PHP, MySQL and MAMP : Part 4. My First Angular-15 Ionic-7 App

Visualize Your Data: Showcasing Interactive Charts for Numerical Data using Charts JS Library (Part 23) in Your Angular-15 Ionic-7 App

How to Build a Unit Converter for Your Baking Needs with HTML, CSS & Vanilla JavaScript