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:
- 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.
- Type imports:
import type { Metadata } from "next";
: This line imports theMetadata
type from the Next.js library. TheMetadata
type is used to define metadata for the web page, such as its title and description.
- Font import:
import { Inter } from "next/font/google";
: This line imports theInter
font from Google Fonts. The font is used in the application to style the text.
- 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.
- Font configuration:
const inter = Inter({ subsets: ["latin"] });
: This line configures theInter
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.
- Metadata definition:
export const metadata: Metadata = {...};
: This code defines a constant variable namedmetadata
that holds metadata information for the web page, including the title and description.
- RootLayout component:
export default function RootLayout({ children }: { children: React.ReactNode; }) { ... }
: This is a functional component namedRootLayout
that serves as the layout for the web page. It takes a single prop calledchildren
, which represents the content that will be rendered within the layout.
- 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 thebody
element to the class name generated by theinter
font, ensuring that theInter
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 theNavBar
component. TheNavBar
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 theContainer
component with the Bootstrap class "py-4" (padding on the y-axis) and places thechildren
prop inside it. TheContainer
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:
use client
: The directive "use client" indicates NextJS that this component will be rendered on client side.- Import statements:
import Link from "next/link";
: This imports theLink
component from Next.js, which is used for client-side navigation. TheLink
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 thereact-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 theusePathname
hook from Next.js'snext/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.
NavBar
component:export default function NavBar() { ... }
: This is a functional component namedNavBar
, which represents the custom navigation bar for the application.
usePathname
hook:const pathname = usePathname();
: This line uses theusePathname
hook to get the current URL pathname. Thepathname
variable will store the current path, which will be used to determine the active state of the navigation link.
- 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.
- Container component:
<Container>
: This is a Bootstrap Container component, which provides a responsive container for the Navbar content.
- 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 aLink
component from Next.js, which allows navigating to the homepage ("/") without a full page refresh when clicked.
- 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.
- Nav component:
<Nav>
: This is a Bootstrap Nav component, which contains the navigation links.
- Nav.Link component:
<Nav.Link as={Link} href="/static" active={pathname === "/static"}>Static</Nav.Link>
: This is a navigation link. It is wrapped in aLink
component and has anactive
prop that checks if the currentpathname
matches the link'shref
. If thepathname
matches thehref
, 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:
- Import statements:
import { UnsplashImage } from "@/models/unsplash-image";
: This imports theUnsplashImage
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 theImage
component from Next.js. TheImage
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 theLink
component from Next.js, which is used for client-side navigation.import { Alert } from "@/components/bootstrap";
: This imports theAlert
component from a custom "@/components/bootstrap" module. TheAlert
component is likely a custom component that provides an alert box with Bootstrap styling.import type { Metadata } from "next";
: This imports theMetadata
type from Next.js, which is used to define metadata for the page, such as its title and other properties.
- Metadata definition:
export const metadata: Metadata = {...};
: This code defines a constant variable namedmetadata
that holds metadata information for the web page. In this case, the page title is set to "Static Photo Page."
- Page component:
export default async function Page() { ... }
: This is an asynchronous function component namedPage
, which is the main component that will be displayed for the static photo page.
- Fetching Unsplash image data:
const response = await fetch("<https://api.unsplash.com/photos/random?client_id=>" + process.env.UNSPLASH_ACCESS_KEY);
: This line uses thefetch
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 theprocess.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 theimage
variable. TheUnsplashImage
type or interface (imported earlier) is used to type theimage
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.
- 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.jsImage
component. It sets thesrc
attribute toimage.urls.raw
, which is the URL of the image provided by the Unsplash API. Thealt
attribute is set toimage.description
, which likely represents a brief description of the image. Thewidth
andheight
attributes are set to the calculated values, ensuring that the image is responsive and proportionally sized. TheclassName
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. Thehref
attribute sets the destination URL, which includes the user's username. The displayed content of the link is the username obtained fromimage.user.username
.
- The returned JSX content is wrapped in a
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:
- Import statements:
import { UnsplashImage } from "@/models/unsplash-image";
: This imports theUnsplashImage
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 theImage
component from Next.js. TheImage
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 theLink
component from Next.js, which is used for client-side navigation.import { Alert } from "@/components/bootstrap";
: This imports theAlert
component from a custom "@/components/bootstrap" module. TheAlert
component is likely a custom component that provides an alert box with Bootstrap styling.import type { Metadata } from "next";
: This imports theMetadata
type from Next.js, which is used to define metadata for the page, such as its title and other properties.
- Metadata definition:
export const metadata: Metadata = {...};
: This code defines a constant variable namedmetadata
that holds metadata information for the web page. In this case, the page title is set to "Dynamic Photo Page."
revalidate
variable:export const revalidate = 0;
: This code defines a constant variable namedrevalidate
and sets it to0
. 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 to0
, it disables revalidation, meaning that the page content won't be revalidated and updated on subsequent requests.
- Page component:
export default async function Page() { ... }
: This is an asynchronous function component namedPage
, which is the main component that will be displayed for the dynamic photo page.
- Fetching Unsplash image data:
const response = await fetch("<https://api.unsplash.com/photos/random?client_id=>" + process.env.UNSPLASH_ACCESS_KEY, { ... });
: This line uses thefetch
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 theprocess.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 theimage
variable. TheUnsplashImage
type or interface (imported earlier) is used to type theimage
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.
- 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.jsImage
component. It sets thesrc
attribute toimage.urls.raw
, which is the URL of the image provided by the Unsplash API. Thealt
attribute is set toimage.description
, which likely represents a brief description of the image. Thewidth
andheight
attributes are set to the calculated values, ensuring that the image is responsive and proportionally sized. TheclassName
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. Thehref
attribute sets the destination URL, which includes the user's username. The displayed content of the link is the username obtained fromimage.user.username
.
- The returned JSX content is wrapped in a
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:
revalidate
variable:export const revalidate = 15;
: This code defines a constant variable namedrevalidate
and sets it to15
. This variable is used to control the revalidation behavior of the page when using Incremental Static Regeneration (ISR) in Next.js. Setting it to15
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.
- 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: Therevalidate
constant is defined to control the revalidation behavior of the page using ISR. Setting it to15
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 anAlert
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. TheImage
component from Next.js is used to render the fetched image, and aLink
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:
- Import statements:
import { UnsplashImage } from "@/models/unsplash-image";
: This imports theUnsplashImage
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 theImage
component from Next.js. TheImage
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 thestyles
object. CSS modules allow locally scoped styles for components to avoid global style conflicts.import { Alert } from "@/components/bootstrap";
: This imports theAlert
component from a custom "@/components/bootstrap" module. TheAlert
component is likely a custom component that provides an alert box with Bootstrap styling.import { Metadata } from "next";
: This imports theMetadata
type from Next.js. TheMetadata
type is used to define metadata for the page, such as its title.
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 atopic
property of typestring
, representing the topic for the page.
- This interface is defined to specify the expected structure of the props that will be passed to the
generateMetadata
function:- This function is responsible for generating the metadata for the page based on the
params
object. It takes theparams
object and extracts thetopic
property to create the page's title. The title is set to be "{topic} - Stock Photo Gallery."
- This function is responsible for generating the metadata for the page based on the
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 thetopic
property. ThegenerateStaticParams
function returns an array of topics (in this case, "health," "cars," and "nature") mapped to the appropriateparams
object.
- 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
- Page component:
- The
Page
component is the main component responsible for rendering the topic page. - It receives the
params
object (with thetopic
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 usinggenerateStaticParams
. It also displays thetopic
as anh1
heading and then maps through theimages
array to render each image using theImage
component from Next.js. The images are displayed with a fixed width and height of 250 pixels and styled using the CSS moduleTopicPage.module.css
.
- The
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:
- Import statements:
import SearchPage from "./SearchPage";
: This imports theSearchPage
component from the file "SearchPage.js" (or "SearchPage.tsx"). TheSearchPage
component is likely a client-side component responsible for displaying a search page.import { Metadata } from "next";
: This imports theMetadata
type from Next.js. TheMetadata
type is used to define metadata for the page, such as its title.import { Alert } from "@/components/bootstrap";
: This imports theAlert
component from a custom "@/components/bootstrap" module. TheAlert
component is likely a custom component that provides an alert box with Bootstrap styling.
- Metadata definition:
export const metadata: Metadata = {...};
: This code defines a constant variable namedmetadata
that holds metadata information for the web page. In this case, the page title is set to "Search - Stock Photo Gallery."
- Page component:
export default function Page() { ... }
: This is the default function component namedPage
, which is the main component that will be displayed for the page.
- 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. TheSearchPage
component is imported and used as a child component here. This component likely handles the client-side rendering and functionality for the search page.
- The returned JSX content is wrapped in a
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:
- Import statements:
import { UnsplashImage } from "@/models/unsplash-image";
: This imports theUnsplashImage
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 theFormEvent
type and theuseState
hook from the React library. TheFormEvent
type is used to define the event object for form submissions, and theuseState
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, includingButton
,Form
, andSpinner
.import Image from "next/image";
: This imports theImage
component from Next.js. TheImage
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 thestyles
object. CSS modules allow locally scoped styles for components to avoid global style conflicts.
- State variables:
const [searchResults, setSearchResults] = useState<UnsplashImage[] | null>([]);
: This creates a state variablesearchResults
and a corresponding functionsetSearchResults
using theuseState
hook. The initial value ofsearchResults
is an empty array, and its type is specified as an array ofUnsplashImage
objects ornull
.const [searchResultsLoading, setSearchResultsLoading] = useState(false);
: This creates a state variablesearchResultsLoading
and a corresponding functionsetSearchResultsLoading
. The initial value ofsearchResultsLoading
isfalse
, indicating that the search results are not currently loading.const [searchResultsLoadingIsError, setSearchResultsLoadingIsError] = useState(false);
: This creates a state variablesearchResultsLoadingIsError
and a corresponding functionsetSearchResultsLoadingIsError
. The initial value ofsearchResultsLoadingIsError
isfalse
, indicating that there is no error with the search results loading.
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 setssearchResults
tonull
to indicate that the search results are being loaded, setssearchResultsLoading
totrue
to trigger the display of a loading spinner, and setssearchResultsLoadingIsError
tofalse
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
totrue
to trigger the display of an error message. - Finally, regardless of the request's outcome, the function sets
searchResultsLoading
tofalse
to hide the loading spinner.
- JSX content:
- The returned JSX content starts with a
Form
element that allows users to input their search query. When the form is submitted, thehandleSubmit
function is called. - A loading spinner (
<Spinner>
) is displayed ifsearchResultsLoading
istrue
. - An error message is displayed if
searchResultsLoadingIsError
istrue
. - 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 thesearchResults
array, and its URL, description, width, and height are provided as props to theImage
component. The images are styled using CSS modules with a fixed width and height of 250 pixels.
- The returned JSX content starts with a
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:
- Import statements:
import { UnsplashImageSearchResponse } from "@/models/unsplash-image";
: This imports theUnsplashImageSearchResponse
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 theNextResponse
object from Next.js server APIs. TheNextResponse
object is used to create custom responses for server routes.
- Route handler function:
export async function GET(request: Request) { ... }
: This code exports an asynchronous function namedGET
that receives arequest
object as an argument. Therequest
object represents the incoming HTTP request to the/api/search
endpoint.
- Parsing the query parameter:
const { searchParams } = new URL(request.url);
: This line creates a new URL object from therequest.url
and extracts thesearchParams
property. ThesearchParams
object contains the query parameters from the URL.const query = searchParams.get("query");
: This line retrieves the value of the "query" parameter from thesearchParams
object. It will contain the user's search query provided in the URL.
- Handling invalid queries:
- The code checks if a valid
query
exists. If not, it returns a custom JSON response usingNextResponse.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.
- The code checks if a valid
- 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 usingfetch
. 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. Theresults
property contains an array of Unsplash images that match the search query.
- If a valid
- Returning the search results:
- The code returns the search results as a JSON response using
NextResponse.json
. Theresults
array is sent as the response body.
- The code returns the search results as a JSON response using
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:
- Import statements:
import Link from "next/link";
: This imports theLink
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, includingNavbar
,Nav
,Container
, andNavDropdown
. These components are used to create a responsive navigation bar with dropdown menus.import { usePathname } from "next/navigation";
: This imports theusePathname
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.
NavBar
component:- The
NavBar
component is a functional component that returns the JSX content for the navigation bar.
- The
pathname
variable:const pathname = usePathname();
: This line initializes a variablepathname
using theusePathname
hook. It holds the current pathname of the URL, which is used to determine the active link in the navigation bar.
- JSX content:
- The returned JSX content starts with the
Navbar
component from React Bootstrap. It sets various props such asbg
,variant
,sticky
,expand
, andcollapseOnSelect
to define the appearance and behavior of the navigation bar. - Within the
Navbar
, there is aContainer
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 multipleNav.Link
components, representing links to different pages. EachNav.Link
is created using theLink
component from Next.js. Theactive
prop of eachNav.Link
is set to determine whether the link should be displayed as active based on the currentpathname
. Theactive
prop istrue
if thepathname
matches the link'shref
, andfalse
otherwise. - There is also a
NavDropdown
component for the "Topics" dropdown menu. It contains severalNavDropdown.Item
components, each representing a link to a specific topic page. TheNavDropdown.Item
links are also created using theLink
component from Next.js, and theactive
prop is used to determine whether a specific topic link is active based on the currentpathname
.
- The returned JSX content starts with the
Link
components inNavDropdown.Item
:- The
NavDropdown.Item
components for different topics use the sameLink
component, but they have differenthref
props that lead to different topic pages ("/topics/health"
,"/topics/cars"
, and"/topics/nature"
). Theactive
prop of eachNavDropdown.Item
is set totrue
when thepathname
matches"/topics/[topic]"
, where[topic]
is a dynamic segment representing any topic name.
- The
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:
- Import statements:
import { UnsplashUser } from "@/models/unsplash-user";
: This imports theUnsplashUser
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 theMetadata
type from Next.js. TheMetadata
type is used to define metadata for the page, such as its title.import { notFound } from "next/navigation";
: This imports thenotFound
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 theAlert
component from a custom "@/components/bootstrap" module. TheAlert
component is likely a custom component that provides an alert box with Bootstrap styling.
PageProps
interface:interface PageProps { params: { username: string }; }
: This code defines an interface namedPageProps
with aparams
property that has ausername
property of type string. This interface will be used to pass the dynamic parameters to the page component.
getUser
function:async function getUser(username: string): Promise<UnsplashUser> { ... }
: This code defines an asynchronous function namedgetUser
that takes ausername
as a parameter and returns a Promise of typeUnsplashUser
. The function fetches user data from the Unsplash API based on the providedusername
.- 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, thenotFound()
function is called to return a 404 response.
generateMetadata
function:export async function generateMetadata({ params: { username } }: PageProps): Promise<Metadata> { ... }
: This code exports an asynchronous function namedgenerateMetadata
. It takes theusername
from thePageProps
interface and returns a Promise of typeMetadata
. 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 providedusername
. - 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.
Page
component:- The
Page
component is a default function component that takes theusername
from thePageProps
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 providedusername
. - The JSX content displays an
Alert
component explaining that the page usesgenerateMetadata
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 theuser.username
as the URL.
- The
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!