DEV Community

Cover image for Build a Web3 Movie Streaming dApp using NextJs, Tailwind, and Sia Renterd: Part Three
Gospel Darlington
Gospel Darlington

Posted on

1

Build a Web3 Movie Streaming dApp using NextJs, Tailwind, and Sia Renterd: Part Three

Let's dive into the final part of this tutorial series, where we'll integrate the backend with the Frontend, connecting the pieces to complete the file upload application. We will begin by ensuring that authentications in the Frontend are up and running.

Web3 Modal Authentication

Create a new folder named 'config' in the Frontend directory and add an index file, resulting in the path /frontend/config/index.tsx. Now let’s add the following codes to it.

// config/index.tsx
// Step 1: Import necessary modules and configurations
import { defaultWagmiConfig } from '@web3modal/wagmi/react/config'
import { cookieStorage, createStorage } from 'wagmi'
import { mainnet, sepolia } from 'wagmi/chains'
// Step 2: Define the WalletConnect Cloud project ID from environment variables
export const projectId = process.env.NEXT_PUBLIC_PROJECT_ID || ''
// Step 3: Create a metadata object for the application
const metadata = {
name: 'VidTv', // Application name
description: 'Stream video directly from the Sia blockchain', // Application description
url: 'https://web3modal.com', // Application URL (must match domain & subdomain)
icons: ['https://avatars.githubusercontent.com/u/37784886'], // Application icons
}
// Step 4: Define the supported chains for the application
const chains = [mainnet, sepolia] as const
// Step 5: Create the Wagmi configuration using defaultWagmiConfig
export const config = defaultWagmiConfig({
chains, // Supported chains
projectId, // WalletConnect Cloud project ID
metadata, // Application metadata
ssr: true, // Enable server-side rendering
storage: createStorage({ // Create storage using cookieStorage
storage: cookieStorage,
}),
auth: { // Authentication settings
email: true, // Enable email authentication (default: true)
socials: ['google', 'x', 'github', 'discord', 'apple'], // Enable social login options
showWallets: false, // Disable showing wallets (default: true)
walletFeatures: true, // Enable wallet features (default: true)
},
})
view raw index.tsx hosted with ❤ by GitHub

This code sets up a Wagmi configuration for our Web3 application, defining metadata, supported chains, and authentication settings, including wallet and social login options, and stores it in the config export. We also need to create a context API to keep track of the authentication state.

The Context API
Next, create a new folder named 'context' still in the Frontend directory and add an index file, resulting in the path /frontend/context/index.tsx. Add the following codes to it.

// context/index.tsx
// Step 1: Enable client-side rendering
'use client'
// Step 2: Import necessary modules and configurations
import React, { ReactNode } from 'react'
import { config, projectId } from '@/config'
import { createWeb3Modal, useWeb3ModalTheme } from '@web3modal/wagmi/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { State, WagmiProvider } from 'wagmi'
// Step 3: Setup query client for React Query
const queryClient = new QueryClient()
// Step 4: Validate project ID
if (!projectId) throw new Error('Project ID is not defined')
// Step 5: Create Web3 modal with Wagmi configuration and project ID
createWeb3Modal({
wagmiConfig: config, // Wagmi configuration
projectId, // Project ID
enableAnalytics: false, // Optional - defaults to your Cloud configuration
enableOnramp: false, // Optional - false as default
})
// Step 6: Define the Web3ModalProvider component
export default function Web3ModalProvider({
children, // Application children
initialState, // Optional initial state
}: {
children: ReactNode
initialState?: State
}) {
// Step 7: Set theme variables for Web3 modal
const { setThemeVariables } = useWeb3ModalTheme()
setThemeVariables({
'--w3m-accent': '#22C55E', // Accent color
'--w3m-color-mix-strength': 0, // Color mix strength
})
// Step 8: Return the application wrapped in WagmiProvider and QueryClientProvider
return (
<WagmiProvider config={config} initialState={initialState}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</WagmiProvider>
)
}
view raw index.tsx hosted with ❤ by GitHub

This code sets up a Web3Modal provider using Wagmi and React Query, configuring the Web3 modal with the project ID and theme variables, and wrapping the application in a WagmiProvider and QueryClientProvider.

Updating Layout
Let’s have our application layout updated to include the above configurations. Head to /frontend/app/layout.tsx and replace its codes with the one below.

// Step 1: Import necessary modules and components
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css' // Global styles
import 'react-toastify/dist/ReactToastify.css' // Toast notifications styles
import 'react-loading-skeleton/dist/skeleton.css' // Skeleton loader styles
import Header from './components/layout/Header' // Header component
import Footer from './components/layout/Footer' // Footer component
import { cookieToInitialState } from 'wagmi' // Convert cookie to initial state
import { config } from '@/config' // Wagmi configuration
import { headers } from 'next/headers' // Next.js headers
import Web3ModalProvider from '@/context' // Web3 modal provider
import { ToastContainer } from 'react-toastify' // Toast notifications container
// Step 2: Create Inter font instance
const inter = Inter({ subsets: ['latin'] })
// Step 3: Define metadata for the application
export const metadata: Metadata = {
title: 'VidTv', // Application title
description: 'Stream video directly from the Sia blockchain', // Application description
}
// Step 4: Define the root layout component
export default function RootLayout({
children, // Application children
}: Readonly<{
children: React.ReactNode
}>) {
// Step 5: Convert cookie to initial state for Web3 modal
const initialState = cookieToInitialState(config, headers().get('cookie'))
// Step 6: Return the application layout
return (
<html lang="en">
<body className={inter.className}>
<Web3ModalProvider initialState={initialState}>
<div className="flex flex-col h-[100vh]">
<Header /> {/* Step 7: Render Header component */}
<main className="flex-grow mt-16 p-4 sm:p-8 max-w-7xl sm:mx-auto w-full">
{children} {/* Step 8: Render application children */}
</main>
<Footer /> {/* Step 9: Render Footer component */}
</div>
<ToastContainer
position="bottom-center" // Step 10: Configure toast notifications
autoClose={5000} // Auto-close delay
hideProgressBar={false} // Show progress bar
newestOnTop={false} // New notifications on top
closeOnClick // Close on click
rtl={false} // Right-to-left layout
pauseOnFocusLoss // Pause on focus loss
draggable // Draggable notifications
pauseOnHover // Pause on hover
theme="dark" // Dark theme
/>
</Web3ModalProvider>
</body>
</html>
)
}
view raw layout.tsx hosted with ❤ by GitHub

The above code sets up the root layout for a Next.js application, including metadata, fonts, styles, and providers for Web3 modal, toast notifications, and layout components like header and footer.

The Login Button
Now, we need to enable the login buttons in the /frontend/app/components/layout/Header.tsx and /frontend/app/components/shared/Menu.tsx components, and update their codes using the information below.

import React from 'react'
import { SiGoogledisplayandvideo360 } from 'react-icons/si'
import Menue from '../shared/Menu'
import Link from 'next/link'
const Header = () => {
return (
<div className="fixed top-0 w-full bg-[#121926] text-slate-200 z-50">
<div className="max-w-7xl mx-auto flex justify-between items-center p-4 px-8">
<Link href="/" className="flex gap-2 items-center text-lg sm:text-2xl ">
<span>
<SiGoogledisplayandvideo360 className="text-green-500" />
</span>
<h1 className=" font-semibold ">VidTV</h1>
</Link>
<div className="flex items-center gap-10">
<ul className="hidden md:flex gap-4 font-medium">
<Link href="/create">Create</Link>
<li>Movies</li>
<li>Series</li>
<li>Anime</li>
<Link href="/account">Account</Link>
</ul>
<Menue />
<div className="hidden md:block">
<w3m-button label="Login" balance="hide" /> {/* Launches the web3 modal */}
</div>
</div>
</div>
</div>
)
}
export default Header
view raw Header.tsx hosted with ❤ by GitHub
'use client'
import Link from 'next/link'
import React, { useState } from 'react'
import { AiOutlineMenu, AiOutlineClose } from 'react-icons/ai'
const Menue = () => {
const [isOpen, setIsOpen] = useState(false)
const toggleMenu = () => {
setIsOpen(!isOpen)
console.log(`Menue is now ${isOpen ? 'Open' : 'Close'}`)
}
return (
<div className="flex items-center">
<button
className="text-gray-500 focus:outline-none md:hidden cursor-pointer"
onClick={toggleMenu}
>
{isOpen ? (
<AiOutlineClose className="h-6 w-6 text-green-500" />
) : (
<AiOutlineMenu className="h-6 w-6 text-green-500" />
)}
</button>
<div
className={`md:hidden ${
isOpen
? 'p-6 flex flex-col items-center gap-4 absolute top-14 left-0 w-full bg-black text-center text-white text-lg '
: 'hidden'
}`}
>
<Link href="/create">Create</Link>
<p>Movies</p>
<p>Series</p>
<p>Anime</p>
<Link href="/account">Account</Link>
<w3m-button label="Login" balance="hide" /> {/* Launches the web3 modal */}
</div>
</div>
)
}
export default Menue
view raw Menu.tsx hosted with ❤ by GitHub

This code defines a React component for a navigation bar that includes a logo, navigation links, a custom menu, and a login button that launches a Web3 Modal, with a responsive design for different screen sizes.

The following images should pop up as proof that what we have done works when you click on the login button and proceed with your preferred provider, X, Facebook, Google, Discord, or Ethereum.

Before Authentication

After Authentication

Superb, let’s go deeper and set up our database and NextJs API-based system. For any confusion on the process, please watch the video section below, just make sure you stop at the 02:57:59 mark.

Database Scripts

First, let’s update the NextJs configuration script to properly address our pages, and endpoints, and free our remote images from warnings and scrutiny.

// Step 1: Define the Next.js configuration object
/** @type {import('next').NextConfig} */
const nextConfig = {
// Step 2: Define rewrites for API routes
rewrites: async () => {
return [
// Step 2.1: Rewrite API routes to match the API path
{
source: '/api/:path*', // Source path
destination: '/api/:path*', // Destination path
},
// Step 2.2: Rewrite client-side routes to match the pages directory
{
source: '/:path*', // Source path
destination: '/pages/:path*', // Destination path
},
]
},
// Step 3: Configure image optimization
images: {
// Step 3.1: Allow remote images from any HTTPS hostname
remotePatterns: [
{
protocol: 'https', // Protocol
hostname: '**', // Allow any hostname
},
],
// Step 3.2: Allow images from localhost domain
domains: ['localhost'], // Domains
},
}
// Step 4: Export the Next.js configuration object
export default nextConfig
view raw next.config.mjs hosted with ❤ by GitHub

This code defines a Next.js configuration object that sets up API route rewrites and image optimization, allowing remote images from any HTTPS hostname and local images from the localhost domain.

Database Config Script
We will be using SQLite for this application, but you are free to use a more robust solution such as MYSQL or NOSQL providers. For the sake of simplicity, let's work with a SQLite flat file.

Create /frontend/app/api/database.ts file path and add the codes below in it.

// Step 1: Import necessary modules
import path from 'path' // Import path module for file path manipulation
import sqlite3 from 'sqlite3' // Import sqlite3 module for database operations
// Step 2: Define the database file path
const dbPath = path.join(process.cwd(), 'movies.db') // Join current working directory with 'movies.db' filename
// Step 3: Create a new sqlite3 database instance
const db = new sqlite3.Database(
dbPath, // Database file path
sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, // Open flags: read-write and create if not exists
(err: any) => { // Callback function for database connection
if (err) { // Check for errors
console.error(err.message) // Log error message
}
console.log('Connected to the movie database.') // Log success message
}
)
// Step 4: Define an API function for GET requests
export const apiGet = async (query: any) => { // Async function for GET requests
return await new Promise((resolve, reject) => { // Create a new promise
db.all(query, (err: Error, row: any) => { // Execute query and get all rows
if (err) { // Check for errors
console.log(err) // Log error
return reject(err) // Reject promise with error
}
return resolve(row) // Resolve promise with query result
})
})
}
// Step 5: Define an API function for POST requests
export const apiPost = async (query: any, values: string[]) => { // Async function for POST requests
return await new Promise((resolve, reject) => { // Create a new promise
db.run(query, values, (err: Error) => { // Execute query with values
if (err) { // Check for errors
console.log(err) // Log error
return reject(err) // Reject promise with error
}
return resolve(null) // Resolve promise with no value
})
})
}
// Step 6: Export the database instance as default
export default db // Export db instance for external use
view raw database.ts hosted with ❤ by GitHub

This code sets up an SQLite database connection and defines two API functions, apiGet and apiPost, to perform GET and POST requests on the database, with error handling and promise-based asynchronous execution. We will be using these codes whenever we wish to send or retrieve data from the database.

Database Migration Script
We need to create both a database flat file and a table to hold all our contents. Create /frontend/app/api/migrations.ts file path and add the codes below in it.

// Step 1: Import the database instance from the database module
import db from './database'
// Step 2: Define a function to migrate the database schema
const migrate = () => {
// Step 3: Serialize database operations to ensure sequential execution
db.serialize(() => {
// Step 4: Create the 'movies' table if it doesn't exist
db.run(
`
CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId TEXT NOT NULL,
name TEXT NOT NULL,
image TEXT NOT NULL,
videoUrl TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
release DATE NOT NULL,
genre TEXT NOT NULL,
rating TEXT NOT NULL,
language TEXT NOT NULL,
duration TEXT NOT NULL,
background TEXT NOT NULL,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`,
// Step 5: Handle errors and log success message
(err: Error) => {
if (err) {
console.error(err.message) // Log error message
} else {
console.log('movies table schema created successfully.') // Log success message
}
}
)
})
}
// Step 6: Call the migrate function to execute the schema migration
migrate()
view raw migrations.ts hosted with ❤ by GitHub

This code defines a database migration function that creates a 'movies' table with specified columns if it doesn't exist, using SQLite, and logs the result of the operation. Now run the command below in a terminal pointed at the /frontend directory.

$ cd frontend
$ npx esrun app/api/migrations.ts
Enter fullscreen mode Exit fullscreen mode

It should be noted that this process will also create a database flat file called movies.db at the root of the frontend directory. We have also added this command to the package.json script, so running $ yarn migrate on the frontend directory should work the same.

For visual assistance, watch the video below, just stop it at the 03:10:54 mark.

Application Endpoints

Now, let’s define some endpoints for creating, reading, updating, and deleting movies, we will be using the NextJs API provision to make these endpoints.

Create Movie Endpoint
To create a movie, the required information includes the user ID, movie name, image, video URL, release date, genre, rating, language, duration, and background description. Create /frontend/app/api/movies/create/route.ts file path and add the codes below in it.

// Step 1: Import necessary modules
import crypto from 'crypto' // Import crypto module for generating random strings
import { apiPost } from '../../database' // Import apiPost function from database module
// Step 2: Define an async function to handle POST requests
export async function POST(req: Request, res: Response) {
// Step 3: Parse the request body as JSON
const content = await req.json()
// Step 4: Define required properties for the movie object
const requiredProperties = [
'userId',
'name',
'image',
'videoUrl',
'release',
'genre',
'rating',
'language',
'duration',
'background',
]
// Step 5: Check for missing required properties
const missingProperty = requiredProperties.find(
(property) => !(property in content)
)
// Step 6: Return an error response if a required property is missing
if (missingProperty) {
return Response.json(
{
error: `Missing required property: ${missingProperty}`,
},
{
status: 400,
headers: { 'content-type': 'application/json' },
}
)
}
// Step 7: Extract movie properties from the request body
const {
userId,
name,
image,
videoUrl,
release,
genre,
rating,
language,
duration,
background,
} = content
// Step 8: Generate a random string for the movie slug
const randomString = crypto.randomBytes(3).toString('hex').toLowerCase()
// Step 9: Create a unique slug for the movie
const slug = `${name
.replace(/[^a-zA-Z0-9 ]/g, '')
.replace(/\s/g, '-')
.split('-')
.join('-')}-${randomString}`.toLowerCase()
// Step 10: Define the SQL query to insert the movie into the database
const query = `
INSERT INTO movies(userId, name, image, videoUrl, slug, release, genre, rating, language, duration, background)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
// Step 11: Define the values to be inserted into the database
const values = [
userId,
name,
image,
videoUrl,
slug,
release,
genre,
rating,
language,
duration,
background,
]
// Step 12: Initialize variables to store the response status and body
let status, body
// Step 13: Execute the SQL query using the apiPost function
await apiPost(query, values)
.then(() => {
// Step 14: Set the response status and body on success
status = 201
body = { message: 'Successfully created movie' }
})
.catch((err) => {
// Step 15: Set the response status and body on error
status = 400
body = err
})
// Step 16: Return the response as JSON
return Response.json(body, {
status,
headers: { 'content-type': 'application/json' },
})
}
view raw route.ts hosted with ❤ by GitHub

This code defines an endpoint to handle POST requests, validate and process movie data, generate a unique slug, and insert the data into a database using an apiPost function while handling errors and returning JSON responses.

Update Movie Endpoint
To update a movie, the required information includes the user ID, slug, and other information provided when creating a movie. Create /frontend/app/api/movies/update/route.ts file path and add the codes below in it.

// Step 1: Import the apiPost function from the database module
import { apiPost } from '../../database'
// Step 2: Define an async function to handle POST requests
export async function POST(req: Request, res: Response) {
// Step 3: Parse the request body as JSON
const content = await req.json()
// Step 4: Define the required properties for the movie update
const requiredProperties = [
'userId',
'slug',
'name',
'image',
'videoUrl',
'release',
'genre',
'rating',
'language',
'duration',
'background',
]
// Step 5: Check for missing required properties
const missingProperty = requiredProperties.find(
(property) => !(property in content)
)
// Step 6: Return an error response if a required property is missing
if (missingProperty) {
return Response.json(
{
error: `Missing required property: ${missingProperty}`,
},
{
status: 400,
headers: { 'content-type': 'application/json' },
}
)
}
// Step 7: Extract movie properties from the request body
const {
userId,
slug,
name,
image,
videoUrl,
release,
genre,
rating,
language,
duration,
background,
} = content
// Step 8: Define the SQL query to update the movie
const query = `
UPDATE movies
SET name = ?, image = ?, videoUrl = ?, release = ?, genre = ?, rating = ?, language = ?, duration = ?, background = ?
WHERE slug = ? AND userId = ?
`
// Step 9: Define the values to be updated in the database
const values = [
name,
image,
videoUrl,
release,
genre,
rating,
language,
duration,
background,
slug,
userId,
]
// Step 10: Initialize variables to store the response status and body
let status, body
// Step 11: Execute the SQL query using the apiPost function
await apiPost(query, values)
.then(() => {
// Step 12: Set the response status and body on success
status = 200
body = { message: 'Successfully updated movie' }
})
.catch((err) => {
// Step 13: Set the response status and body on error
status = 400
body = err
})
// Step 14: Return the response as JSON
return Response.json(body, {
status,
headers: { 'content-type': 'application/json' },
})
}
view raw route.ts hosted with ❤ by GitHub

This code defines an endpoint to handle POST requests for updating a movie, validating required properties, and executing an SQL query to update the movie data in the database using the apiPost function.

Delete Movie Endpoint
To delete a movie, the required information includes the user ID and slug of a movie. Create /frontend/app/api/movies/delete/route.ts file path and add the codes below in it.

// Step 1: Import the apiPost function from the database module
import { apiPost } from '../../database'
// Step 2: Define an async function to handle POST requests for deleting a movie
export async function POST(req: Request, res: Response) {
// Step 3: Parse the request body as JSON
const content = await req.json()
// Step 4: Define the required properties for the movie deletion
const requiredProperties = ['userId', 'slug']
// Step 5: Check for missing required properties
const missingProperty = requiredProperties.find(
(property) => !(property in content)
)
// Step 6: Return an error response if a required property is missing
if (missingProperty) {
return Response.json(
{
error: `Missing required property: ${missingProperty}`,
},
{
status: 400,
headers: { 'content-type': 'application/json' },
}
)
}
// Step 7: Extract the userId and slug from the request body
const { userId, slug } = content
// Step 8: Define the SQL query to delete the movie
const query = `DELETE FROM movies WHERE slug = ? AND userId = ?`
// Step 9: Define the values to be used in the SQL query
const values = [slug, userId]
// Step 10: Initialize variables to store the response status and body
let status, body
// Step 11: Execute the SQL query using the apiPost function
await apiPost(query, values)
.then(() => {
// Step 12: Set the response status and body on success
status = 200
body = { message: 'Successfully deleted movie' }
})
.catch((err) => {
// Step 13: Set the response status and body on error
status = 400
body = err
})
// Step 14: Return the response as JSON
return Response.json(body, {
status,
headers: { 'content-type': 'application/json' },
})
}
view raw route.ts hosted with ❤ by GitHub

This code defines an endpoint to handle POST requests for deleting a movie, validating required properties (userId and slug), and executing an SQL query to delete the movie from the database using the apiPost function.

All Movies Endpoint
The optional data required to get movies are pageSize and userId, which can be passed as query parameters to filter and paginate the results. Create /frontend/app/api/movies/all/route.ts file path and add the codes below in it.

// Step 1: Import the apiGet function from the database module
import { apiGet } from '../../database'
// Step 2: Define an async function to handle GET requests for retrieving movies
export async function GET(req: Request, res: Response) {
// Step 3: Parse the URL and extract query parameters
const url = new URL(req.url)
const pageSizeParam = url.searchParams.get('pageSize')
const userIdParam = url.searchParams.get('userId')
// Step 4: Convert query parameters to appropriate data types
const pageSize = pageSizeParam ? parseInt(pageSizeParam, 10) : null
const userId = userIdParam ? userIdParam : null
// Step 5: Initialize the SQL query to retrieve movies
let query = `SELECT * FROM movies`
// Step 6: Conditionally add filters to the query based on query parameters
if (userId !== null) query += ` WHERE userId="${userId}"`
if (pageSize !== null) query += ` LIMIT ${pageSize}`
// Step 7: Initialize variables to store the response status and body
let status, body
// Step 8: Execute the SQL query using the apiGet function
await apiGet(query)
.then((res) => {
// Step 9: Set the response status and body on success
status = 200
body = res
})
.catch((err) => {
// Step 10: Set the response status and body on error
status = 400
body = err
})
// Step 11: Return the response as JSON
return Response.json(body, {
status,
headers: { 'content-type': 'application/json' },
})
}
view raw route.ts hosted with ❤ by GitHub

The above code defines an endpoint to handle GET requests for retrieving movies, allowing optional filtering by userId and pagination by pageSize, and returns the results in JSON format.

Single Movie Endpoint
To retrieve a single movie, the required data is the slug of a movie. Create /frontend/app/api/movies/[slug]/route.ts file path and add the codes below in it.

// Step 1: Import the apiGet function from the database module
import { apiGet } from '../../database'
// Step 2: Define an async function to handle GET requests for retrieving a movie by slug
export async function GET(
req: Request,
{ params }: { params: any },
res: Response
) {
// Step 3: Extract the slug parameter from the request parameters
const slug = params.slug
// Step 4: Check if the slug parameter is present, return an error if not
if (!slug) {
return Response.json(
{ error: 'slug is required' },
{
status: 400,
headers: { 'content-type': 'application/json' },
}
)
}
// Step 5: Define the SQL query to retrieve the movie by slug
const query = `SELECT * FROM movies WHERE slug = "${slug}"`
// Step 6: Initialize variables to store the response status and body
let status, body
// Step 7: Execute the SQL query using the apiGet function
await apiGet(query)
.then((res) => {
// Step 8: Set the response status and body on success
status = 200
body = res
})
.catch((err) => {
// Step 9: Set the response status and body on error
status = 400
body = err
})
// Step 10: Return the response as JSON
return Response.json(body, {
status,
headers: { 'content-type': 'application/json' },
})
}
view raw route.ts hosted with ❤ by GitHub

This code defines an endpoint to handle GET requests for retrieving a movie by its slug, validating the slug parameter, and executing an SQL query to retrieve the movie data from the database using the apiGet function.

That marks all the endpoints we will need for this application. If you need a visual aid to help you understand these endpoints better, please watch the video below, just ensure you stop at the 03:48:22 timestamp.

Endpoint Integration

Our task is to review and update pre-coded components and pages, explaining each one's purpose and functionality, and documenting the changes we make to the existing code. We will start by creating a service for interacting with the endpoints we previously created in the api directory.

Create /frontend/app/services/api.service.ts file path and add the codes below in it.

import { PosterInterface } from '@/utils/interfaces'
import axios, { AxiosProgressEvent, AxiosRequestConfig } from 'axios'
const fetchMovies = async (
pageSize: number | null = null,
userId: string | null = null
) => {
let url = '/api/movies/all'
if (pageSize !== null) url += `?pageSize=${pageSize}`
if (userId !== null) {
if (url.includes('?')) {
url += `&userId=${userId}`
} else {
url += `?userId=${userId}`
}
}
const res = await fetch(url, {
cache: 'no-store',
})
const moviesData: PosterInterface[] = await res.json()
return moviesData
}
const fetchMovie = async (slug: string) => {
let url = `/api/movies/${slug}`
const res = await fetch(url, {
cache: 'no-store',
})
const moviesData: PosterInterface[] = await res.json()
return moviesData[0]
}
const createMovie = async (movieData: any) => {
try {
const response = await fetch('/api/movies/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(movieData),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const responseData = await response.json()
return responseData
} catch (error) {
console.error('There was a problem with the fetch operation:', error)
throw error
}
}
const updateMovie = async (movieData: PosterInterface) => {
try {
const response = await fetch('/api/movies/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(movieData),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const responseData = await response.json()
return responseData
} catch (error) {
console.error('There was a problem with the fetch operation:', error)
throw error
}
}
const deleteMovie = async (movieData: PosterInterface) => {
try {
const response = await fetch('/api/movies/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(movieData),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const responseData = await response.json()
return responseData
} catch (error) {
console.error('There was a problem with the fetch operation:', error)
throw error
}
}
const uploadFile = async (
file: File,
onProgress: (progressEvent: AxiosProgressEvent) => void
) => {
const url = process.env.NEXT_PUBLIC_FILE_SERVICE_URL + '/upload'
const formData = new FormData()
formData.append('file', file)
const config: AxiosRequestConfig<FormData> = {
method: 'POST',
url,
headers: {
'Content-Type': 'multipart/form-data',
},
data: formData,
onUploadProgress: onProgress,
}
try {
const response = await axios.request(config)
return Promise.resolve(response.data)
} catch (error) {
return Promise.reject(error)
}
}
export { fetchMovies, fetchMovie, createMovie, updateMovie, deleteMovie, uploadFile }
view raw api.service.ts hosted with ❤ by GitHub

This service provides a set of functions to interact with a movie database, allowing the application to fetch movies, fetch a single movie by slug, create a new movie, update an existing movie, delete a movie, and upload files, using API requests and handling errors.

Application Pages

Let’s review and update the various pages associated with our application. You wouldn’t need to change many things, just the ones highlighted here.

Create Movie Page

Create Movie Page

This page is a movie publishing form that allows users to upload video and image files, input movie details, and submit the form to publish the movie, with validation and error handling, using React and Wagmi libraries.

Now, update the file found in /frontend/app/pages/create/page.tsx with the codes below.

'use client'
import { useState } from 'react'
import { toast } from 'react-toastify'
import Uploader from '@/app/components/shared/Uploader'
import Uploaded from '@/app/components/shared/Uploaded'
import { createMovie } from '@/app/services/api.service'
import { useAccount } from 'wagmi'
interface FilesState {
image: string | File
video: string | File
}
export default function Page() {
const { address, isDisconnected } = useAccount()
const [files, setFiles] = useState<FilesState>({
image: '',
video: '',
})
const [movieDetails, setMovieDetails] = useState({
name: '',
image: '',
videoUrl: '',
background: '',
genre: '',
duration: '',
release: '',
rating: '',
language: '',
})
const handleSelectedFile = (name: string, value: string) => {
console.log(name, value)
setFiles((prevDetails) => ({
...prevDetails,
[name]: value,
}))
}
const isAllFieldsFilled = () =>
Object.values(movieDetails).every((value) => value.trim() !== '')
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (!isAllFieldsFilled)
return toast.error('Please fill in all movie details.')
await toast.promise(
new Promise<void>(async (resolve, reject) => {
createMovie({ ...movieDetails, userId: address })
.then((res) => {
resetForm()
resolve(res)
})
.catch((error) => reject(error))
}),
{
pending: 'Publishing...',
success: 'Movie published successful 👌',
error: 'Encountered error 🤯',
}
)
}
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target
setMovieDetails((prevDetails) => ({
...prevDetails,
[name]: value,
}))
}
const handleURLMount = (name: string, value: string) => {
setMovieDetails((prevDetails) => ({
...prevDetails,
[name]: value,
}))
}
const resetForm = () => {
setMovieDetails({
name: '',
image: '',
videoUrl: '',
background: '',
genre: '',
duration: '',
release: '',
rating: '',
language: '',
})
}
return (
<div className="flex flex-col items-center">
<div className="bg-gray-800 bg-opacity-75 border border-slate-500 w-full md:w-2/5 p-4 rounded-xl text-slate-200">
<div className="flex flex-col items-center justify-center gap-2">
<div className="flex justify-between items-center space-x-2 w-full">
{!movieDetails.videoUrl ? (
<Uploader
name="Video"
type="video/mp4"
size={100}
onUploadSuccess={(response) =>
handleURLMount('videoUrl', response.url)
}
onFileSelected={(file) => handleSelectedFile('video', file)}
/>
) : (
<Uploaded
name="Video"
file={files.video}
onRemove={() => handleURLMount('videoUrl', '')}
/>
)}
{!movieDetails.image ? (
<Uploader
name="Poster"
type="image/png, image/jpg, image/jpeg"
size={2}
onUploadSuccess={(response) =>
handleURLMount('image', response.url)
}
onFileSelected={(file) => handleSelectedFile('image', file)}
/>
) : (
<Uploaded
name="Poster"
file={files.image}
onRemove={() => handleURLMount('image', '')}
/>
)}
</div>
<form onSubmit={handleSubmit} className="w-full mt-4">
<div className="mb-4">
<label className="block text-sm font-medium text-slate-200 mb-1">
Name
</label>
<input
type="text"
name="name"
placeholder="E.g. Batman Return"
value={movieDetails.name}
onChange={handleInputChange}
className="w-full p-2 bg-gray-700 border border-slate-500 rounded text-sm"
required
/>
</div>
<div className="flex justify-between items-center space-x-2 mb-4">
<div className="w-full">
<label className="block text-sm font-medium text-slate-200 mb-1">
Genres
</label>
<input
type="text"
name="genre"
placeholder="E.g. Action"
value={movieDetails.genre}
onChange={handleInputChange}
className="w-full p-2 bg-gray-700 border border-slate-500 rounded text-sm"
required
/>
</div>
<div className="w-full">
<label className="block text-sm font-medium text-slate-200 mb-1">
Language
</label>
<input
type="text"
name="language"
placeholder="E.g. Russian"
value={movieDetails.language}
onChange={handleInputChange}
className="w-full p-2 bg-gray-700 border border-slate-500 rounded text-sm"
required
/>
</div>
</div>
<div className="flex justify-between items-center space-x-2 mb-4">
<div className="w-full">
<label className="block text-sm font-medium text-slate-200 mb-1">
Duration
</label>
<input
type="text"
name="duration"
placeholder="E.g. 2h 19m"
value={movieDetails.duration}
onChange={handleInputChange}
className="w-full p-2 bg-gray-700 border border-slate-500 rounded text-sm"
required
/>
</div>
<div className="w-full">
<label className="block text-sm font-medium text-slate-200 mb-1">
Release
</label>
<input
type="text"
name="release"
placeholder="E.g. 2020"
value={movieDetails.release}
onChange={handleInputChange}
className="w-full p-2 bg-gray-700 border border-slate-500 rounded text-sm"
required
/>
</div>
<div className="w-full">
<label className="block text-sm font-medium text-slate-200 mb-1">
Rating
</label>
<input
type="text"
name="rating"
placeholder="E.g. PG"
value={movieDetails.rating}
onChange={handleInputChange}
className="w-full p-2 bg-gray-700 border border-slate-500 rounded text-sm"
required
/>
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-slate-200 mb-1">
Background
</label>
<input
type="text"
name="background"
placeholder="Synopsis of the movie..."
value={movieDetails.background}
onChange={handleInputChange}
className="w-full p-2 bg-gray-700 border border-slate-500 rounded text-sm"
/>
<small className="text-slate-400">
Keep all inputs short and simple no lengthy words needed.
</small>
</div>
{!isDisconnected && address && isAllFieldsFilled() && (
<button
className="w-full bg-green-500 text-white py-2.5 rounded-lg hover:bg-transparent
hover:border-green-800 border border-transparent hover:text-green-500"
>
Publish
</button>
)}
</form>
</div>
</div>
</div>
)
}
view raw page.tsx hosted with ❤ by GitHub

The changes made in this code compared to the original one are:

  1. Imported the createMovie function from api.service and used it in the handleSubmit function to create a new movie.
  2. Added the userId parameter to the createMovie function call, passing the user's address from the useAccount hook.
  3. Updated the handleSubmit function to use toast.promise to handle the promise returned by createMovie.
  4. Added error handling to the createMovie function call in the handleSubmit function.

These changes enable the form to submit movie data to the API and create a new movie entry, while also handling errors and displaying a success message.

Edit Movie Page

Edit Movie Page Similar to the Create Movie Page

This movie editing page allows authorized users to update movie details, upload posters and videos, and save changes, with validation and error handling, utilizing React, Wagmi, and Next.js, specifically designed for users to edit their movies.

Now, update the file found in /frontend/app/pages/movies/edit/[slug]/page.tsx with the codes below.

'use client'
import { useEffect, useState } from 'react'
import { toast } from 'react-toastify'
import { useParams } from 'next/navigation'
import { PosterInterface } from '@/utils/interfaces'
import Link from 'next/link'
import Uploader from '@/app/components/shared/Uploader'
import Uploaded from '@/app/components/shared/Uploaded'
import { posters } from '@/app/data/posters'
import { fetchMovie, updateMovie } from '@/app/services/api.service'
import { useAccount } from 'wagmi'
interface FilesState {
image: string | File
video: string | File
}
export default function Page() {
const { slug } = useParams()
const { address, isDisconnected, isConnecting } = useAccount()
const [movie, setMovie] = useState<PosterInterface | null>(null)
const [files, setFiles] = useState<FilesState>({
image: '',
video: '',
})
const [movieDetails, setMovieDetails] = useState({
name: '',
image: '',
videoUrl: '',
background: '',
genre: '',
duration: '',
release: '',
rating: '',
language: '',
})
useEffect(() => {
const fetchMovieData = async () => {
const movieData = await fetchMovie(slug as string)
if (!movieData) return
setMovieDetails(movieData as any)
setMovie(movieData as PosterInterface)
handleSelectedFile('image', movieData.image as string)
handleSelectedFile('videoUrl', movieData.videoUrl as string)
}
fetchMovieData()
}, [slug, address, isDisconnected])
const handleSelectedFile = (name: string, value: string) => {
setFiles((prevDetails) => ({
...prevDetails,
[name]: value,
}))
}
const isAllFieldsFilled = () =>
Object.values(movieDetails).every((value) => value !== '')
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target
setMovieDetails((prevDetails) => ({
...prevDetails,
[name]: value,
}))
}
const handleURLMount = (name: string, value: string) => {
setMovieDetails((prevDetails) => ({
...prevDetails,
[name]: value,
}))
}
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (!isAllFieldsFilled()) return toast.warning('Some fields are empty')
await toast.promise(
new Promise<void>(async (resolve, reject) => {
updateMovie({ ...movie, ...movieDetails } as PosterInterface)
.then((res) => resolve(res))
.catch((error) => reject(error))
}),
{
pending: 'Updating...',
success: 'Movie updated successful 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<div className="flex flex-col items-center">
<Link href={'/movies/' + movie?.slug} className="mb-4 text-green-500">
Back to movie
</Link>
<div className="bg-gray-800 bg-opacity-75 border border-slate-500 w-full md:w-2/5 p-4 rounded-xl text-slate-200">
<div className="flex flex-col items-center justify-center gap-2">
<div className="flex justify-between items-center space-x-2 w-full">
{!movieDetails.videoUrl ? (
<Uploader
name="Video"
type="video/mp4"
size={100}
onUploadSuccess={(response) =>
handleURLMount('videoUrl', response.url)
}
onFileSelected={(file) => handleSelectedFile('video', file)}
/>
) : (
<Uploaded
name="Video"
file={files.video}
onRemove={() => handleURLMount('videoUrl', '')}
/>
)}
{!movieDetails.image ? (
<Uploader
name="Poster"
type="image/png, image/jpg, image/jpeg"
size={2}
onUploadSuccess={(response) =>
handleURLMount('image', response.url)
}
onFileSelected={(file) => handleSelectedFile('image', file)}
/>
) : (
<Uploaded
name="Poster"
file={files.image}
onRemove={() => handleURLMount('image', '')}
/>
)}
</div>
<form onSubmit={handleSubmit} className="w-full mt-4">
<div className="mb-4">
<label className="block text-sm font-medium text-slate-200 mb-1">
Name
</label>
<input
type="text"
name="name"
placeholder="E.g. Batman Return"
value={movieDetails.name}
onChange={handleInputChange}
className="w-full p-2 bg-gray-700 border border-slate-500 rounded text-sm"
required
/>
</div>
<div className="flex justify-between items-center space-x-2 mb-4">
<div className="w-full">
<label className="block text-sm font-medium text-slate-200 mb-1">
Genres
</label>
<input
type="text"
name="genre"
placeholder="E.g. Action"
value={movieDetails.genre}
onChange={handleInputChange}
className="w-full p-2 bg-gray-700 border border-slate-500 rounded text-sm"
required
/>
</div>
<div className="w-full">
<label className="block text-sm font-medium text-slate-200 mb-1">
Language
</label>
<input
type="text"
name="language"
placeholder="E.g. Russian"
value={movieDetails.language}
onChange={handleInputChange}
className="w-full p-2 bg-gray-700 border border-slate-500 rounded text-sm"
required
/>
</div>
</div>
<div className="flex justify-between items-center space-x-2 mb-4">
<div className="w-full">
<label className="block text-sm font-medium text-slate-200 mb-1">
Duration
</label>
<input
type="text"
name="duration"
placeholder="E.g. 2h 19m"
value={movieDetails.duration}
onChange={handleInputChange}
className="w-full p-2 bg-gray-700 border border-slate-500 rounded text-sm"
required
/>
</div>
<div className="w-full">
<label className="block text-sm font-medium text-slate-200 mb-1">
Release
</label>
<input
type="text"
name="release"
placeholder="E.g. 2020"
value={movieDetails.release}
onChange={handleInputChange}
className="w-full p-2 bg-gray-700 border border-slate-500 rounded text-sm"
required
/>
</div>
<div className="w-full">
<label className="block text-sm font-medium text-slate-200 mb-1">
Rating
</label>
<input
type="text"
name="rating"
placeholder="E.g. PG"
value={movieDetails.rating}
onChange={handleInputChange}
className="w-full p-2 bg-gray-700 border border-slate-500 rounded text-sm"
required
/>
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-slate-200 mb-1">
Background
</label>
<input
type="text"
name="background"
placeholder="Synopsis of the movie..."
value={movieDetails.background}
onChange={handleInputChange}
className="w-full p-2 bg-gray-700 border border-slate-500 rounded text-sm"
/>
<small className="text-slate-400">
Keep all inputs short and simple no lengthy words needed.
</small>
</div>
{movie && movie.userId === address && !isDisconnected && (
<button
className="w-full bg-green-500 text-white py-2.5 rounded-lg hover:bg-transparent
hover:border-green-800 border border-transparent hover:text-green-500"
>
Update
</button>
)}
</form>
</div>
</div>
</div>
)
}
view raw page.tsx hosted with ❤ by GitHub

The upgrades made to the code that is different from the original are:

  1. Imported fetchMovie and updateMovie functions from @/app/services/api.service and used them in the useEffect hook and handleSubmit function, respectively.
  2. Replaced the posters.find() method with the fetchMovie function to retrieve movie data.
  3. Updated the handleSubmit function to call the updateMovie function with the updated movie details.
  4. Added error handling to the updateMovie function call in the handleSubmit function.

These changes enable our application to interact with our API endpoints to retrieve and update movie data, whereas the original code relied on our local posters array.

Home Page

Home Page

This home page renders the banners component, a list of movies (either from an API source or a loading UI), and subscription options, utilizing React and Next.js, to provide an engaging and informative landing page for users.

Update the file found in /frontend/app/pages/page.tsx with the following codes.

'use client'
import React, { useEffect, useState } from 'react'
import Banners from '@/app/components/home/Banners'
import Posters from '@/app/components/home/Posters'
import Subscriptions from '@/app/components/home/Subscriptions'
import { PosterInterface } from '@/utils/interfaces'
import PostersUI from './components/home/PostersUI'
import { fetchMovies } from './services/api.service'
const Page = () => {
const [movies, setMovies] = useState<PosterInterface[]>([])
const [loaded, setLoaded] = useState<boolean>(false)
useEffect(() => {
const fetchMoviesData = async () => {
const moviesData = await fetchMovies()
setMovies(moviesData)
setLoaded(true)
}
fetchMoviesData()
}, [])
return (
<>
<Banners />
{loaded ? <Posters movies={movies} /> : <PostersUI posters={3} />}
<Subscriptions />
</>
)
}
export default Page
view raw page.tsx hosted with ❤ by GitHub

The changes we made to the home page are:

  1. Imported fetchMovies function from ./services/api.service and used it in the useEffect hook to retrieve movie data from our API.
  2. Replaced the local posters data with the fetchMovies function call, which fetches data from our API.
  3. Added await keyword to wait for the promise returned by fetchMovies to resolve before setting the movies state.

These changes help our application to retrieve movie data from our API instead of relying on local data, making the application more dynamic and data-driven.

User Account Page

Account Page

This page displays a list of movies posted by the currently connected user, with a loading skeleton placeholder while the data is being fetched, and a message prompting the user to connect their account if they haven't done so, utilizing Wagmi, and react-loading-skeleton.

Update the file found in /frontend/app/pages/account/page.tsx with the following codes.

'use client'
import MovieCard from '@/app/components/shared/MovieCard'
import { useEffect, useState } from 'react'
import { PosterInterface } from '@/utils/interfaces'
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'
import { fetchMovies } from '@/app/services/api.service'
import { useAccount } from 'wagmi'
const Page = () => {
const [loaded, setLoaded] = useState(false)
const [movies, setMovies] = useState<PosterInterface[]>([])
const { address, isConnecting, isDisconnected } = useAccount()
useEffect(() => {
const fetchMovieData = async () => {
const moviesData = await fetchMovies(null, address)
setMovies(moviesData)
setLoaded(true)
}
fetchMovieData()
}, [address, isConnecting])
return (
<div className="flex flex-col w-full items-center">
<h3 className="text-lg mb-4">
{!loaded
? 'Please connect your account to view your movies.'
: movies.length < 1
? 'You have no movies posted yet...'
: 'Your Movies'}
</h3>
{loaded && (
<ul className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-5 h-full item-center ">
{movies.map((movie) => (
<MovieCard
key={movie.id}
movie={movie}
width="w-52 rounded-2xl"
height="h-96"
/>
))}
</ul>
)}
{!loaded && (
<SkeletonTheme
baseColor="#202020"
highlightColor="#444"
borderRadius={15}
>
<ul className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-5 w-full h-full">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} height={400} width={200} />
))}
</ul>
</SkeletonTheme>
)}
</div>
)
}
export default Page
view raw page.tsx hosted with ❤ by GitHub

The changes made to the page are:

  1. Imported fetchMovies function from @/app/services/api.service and used it in the useEffect hook to retrieve movie data from our API.
  2. Replaced the local posters data with the fetchMovies function call, which fetches data from our API.
  3. Passed address as an argument to the fetchMovies function to retrieve user-specific movie data.
  4. Removed the conditional check for address before rendering the movie list, as the fetchMovies function now handles this logic.
  5. Simplified the conditional statement for displaying the loading skeleton, as it now only depends on the loaded state.

These changes retrieve movie data from our API, specific to the connected user, and display a loading skeleton while the data is being fetched.

Movies Details Page

Movies Details

This page displays a single movie's details, including its name, release year, rating, duration, genre, and background information, along with a video player and related movies, and provides options to edit or delete the movie if the user is the owner, utilizing Next.js, and Wagmi.

Update the file found in /frontend/app/pages/movies/[slug]/page.tsx with the following codes.

'use client'
import { FaStar, FaEdit, FaTrash } from 'react-icons/fa'
import MovieCard from '@/app/components/shared/MovieCard'
import { useParams } from 'next/navigation'
import ReactPlayer from 'react-player'
import { useEffect, useState } from 'react'
import { PosterInterface } from '@/utils/interfaces'
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'
import Link from 'next/link'
import { useAccount } from 'wagmi'
import { toast } from 'react-toastify'
import {
deleteMovie,
fetchMovie,
fetchMovies,
} from '@/app/services/api.service'
const Page = () => {
const { slug } = useParams()
const { address, isDisconnected, isConnecting } = useAccount()
const [loaded, setLoaded] = useState(false)
const [movie, setMovie] = useState<PosterInterface | null>(null)
const [movies, setMovies] = useState<PosterInterface[]>([])
useEffect(() => {
const fetchMovieData = async () => {
const movieData = await fetchMovie(slug as string)
setMovie(movieData as PosterInterface)
const moviesData = await fetchMovies(3)
setMovies(moviesData)
setLoaded(true)
}
fetchMovieData()
}, [slug, address, isDisconnected])
const handleSubmit = async () => {
if (!window.confirm('Are you sure you want to delete this movie?')) return
await toast.promise(
new Promise<void>(async (resolve, reject) => {
deleteMovie(movie as PosterInterface)
.then((res) => {
window.location.href = '/account'
resolve(res)
})
.catch((error) => reject(error))
}),
{
pending: 'Deleting...',
success: 'Movie deleted successful 👌',
error: 'Encountered error 🤯',
}
)
}
const truncateAddress = (address: string): string => {
const startLength = 4
const endLength = 4
const addressString = String(address)
const truncatedAddress = `${addressString.substring(
0,
startLength
)}...${addressString.substring(addressString.length - endLength)}`
return truncatedAddress
}
return (
<div
className="flex flex-col md:flex-row w-full md:items-start items-center
md:justify-between justify-center text-gray-200 md:space-x-6 space-x-0
space-y-6 md:space-y-0 "
>
<div className="w-full sm:w-2/3 space-y-4">
<div className="flex items-center justify-start">
{loaded ? (
<div className="flex justify-start items-center space-x-2">
<div className="flex justify-start items-center space-x-2">
<h2 className="text-2xl">
{movie?.name} ({movie?.release})
</h2>
<FaStar className="text-yellow-500" />
</div>
<div className="flex justify-start items-center space-x-2 text-gray-400">
<p className="text-sm">
{movie?.rating} {movie?.duration}, {movie?.genre}
{' | ' + truncateAddress(movie?.userId || '')}
</p>
{!isDisconnected && address && movie?.userId === address && (
<div className="flex space-x-4">
<span className="w-1"></span>
<Link
href={'/movies/edit/' + movie?.slug}
className="flex justify-start items-center space-x-1 text-green-500"
>
<FaEdit /> <span>Edit</span>
</Link>
<button
onClick={handleSubmit}
className="flex justify-start items-center space-x-1 text-red-500"
>
<FaTrash /> <span>Delete</span>
</button>
</div>
)}
</div>
</div>
) : (
<h4>Loading...</h4>
)}
</div>
<SkeletonTheme
baseColor="#202020"
highlightColor="#444"
borderRadius={15}
>
<div className="w-full h-[450px] rounded-2xl overflow-hidden relative">
{loaded ? (
<ReactPlayer
url={movie?.videoUrl}
width="100%"
height="100%"
className="absolute top-0 left-0"
controls={true}
playing={false}
preload="none"
/>
) : (
<Skeleton height={500} />
)}
</div>
</SkeletonTheme>
<p className="text-gray-400">{movie?.background}</p>
</div>
<div className="w-full sm:w-1/3 space-y-4">
<h3 className="text-lg">Related Movies</h3>
<div className="h-[450px] overflow-y-auto">
{loaded ? (
<ul className="flex flex-col gap-5">
{movies.map((movie) => (
<MovieCard
key={movie.id}
movie={movie}
width="w-full rounded-2xl"
height="h-40"
/>
))}
</ul>
) : (
<SkeletonTheme
baseColor="#202020"
highlightColor="#444"
borderRadius={15}
>
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} height={150} />
))}
</SkeletonTheme>
)}
</div>
</div>
</div>
)
}
export default Page
view raw page.tsx hosted with ❤ by GitHub

We made some huge changes here! Here's a summary of what we did:

  1. Imported deleteMovie, fetchMovie, and fetchMovies functions from @/app/services/api.service and used them to interact with our API endpoints.
  2. Replaced local data with API calls to retrieve movie data.
  3. Implemented movie deletion functionality using the deleteMovie function.
  4. Used toast.promise to display a notification while deleting a movie.
  5. Removed the posters local data and replaced it with API calls.
  6. Updated the handleSubmit function to call the deleteMovie function and handle the response.
  7. Updated the useEffect hook to call the fetchMovie and fetchMovies functions.

These changes cause our application to interact with our API to retrieve and delete movie data and display notifications to the user during the deletion process.

This part of the video below will show you hands-on how we integrated these pages with the endpoint, please feel free to watch that part if you run into any problem. Just make sure you stop at the 04:57:41 timestamp.

Application Components

Let’s discuss the purpose of each component in our application. We will update any component that needs to be modified.

Banner Component

Banner Component

This component displays a rotating background image of movie banners, cycling through an array of movie images every 5 seconds, creating a simple and automatic slideshow effect. This component code can be assessed at /frontend/app/components/home/Banner.tsx.

Posters Component

Posters Component

This component displays a responsive and interactive carousel of movie posters, using the Swiper library, with features like autoplay, pagination, and navigation, showcasing a list of movies passed as a prop, with a dynamic layout adapting to different screen sizes. This component code can be assessed at /frontend/app/components/home/Posters.tsx.

Poster UI Component

Poster UI Component

This component displays a placeholder skeleton layout for a movie posters section, using the react-loading-skeleton library, showing a dynamic number of skeleton posters based on the "posters" prop, with a responsive design adapting to different screen sizes, indicating a loading state until the actual posters data is fetched and displayed. This component code can be assessed at /frontend/app/components/home/PosterUI.tsx.

Subscriptions Component

Subscriptions Component

This component displays a subscription plans section, showcasing various dummy plans with their details, prices, and benefits. It allows users to choose a plan that suits their needs, utilizing a responsive grid layout and interactive hover effects to enhance the user experience. This component code can be assessed at /frontend/app/components/home/Subscription.tsx.

Header Component

The Header Component

This component renders a fixed navigation bar at the top of the page, featuring a logo, a navigation menu with links to various sections, a menu toggle button for responsive design, and a login button, providing a consistent and accessible header section across the application. This component code can be assessed at /frontend/app/components/layout/Header.tsx.

Footer Component

Footer Component

This component renders a footer section at the bottom of the page, featuring the application's logo, a brief description, navigation links, contact information, and a credit mentioning the decentralized storage solution powered by Sia Foundation, providing a clear and organized footer section with relevant information and links. This component code can be assessed at /frontend/app/components/layout/Footer.tsx.

Menu Component

Menu Component

This component renders a responsive menu toggle button, which when clicked, opens or closes a dropdown menu containing navigation links, allowing users to access various sections of the application on smaller screens while hiding the menu on larger screens where the navigation links are already visible. This component code can be assessed at /frontend/app/components/shared/Menu.tsx.

Movie Card Component

Movie Card Component

This component displays a single movie's poster with a hover effect, showing additional information such as the movie's name, release year, and background summary, while also serving as a link to the movie's details page, utilizing a responsive design and animated transitions to enhance the user experience. This component code can be assessed at /frontend/app/components/shared/MovieCard.tsx.

Uploaded Component

Uploaded Component

This component displays a preview of an uploaded file, either an image or a video, with a progress bar and a removal button, allowing users to review and delete the uploaded file, while also providing a visually appealing and interactive interface with animations and hover effects. This component code can be assessed at /frontend/app/components/shared/Uploaded.tsx.

Uploader Component

Uploader Component

This component provides a user interface for uploading files, specifically videos or posters, with features like drag-and-drop, file type validation, size limits, upload progress tracking, and success/error notifications, utilizing a combination of React state management, event handling, and API integration to handle the upload process.

Update the file found in /frontend/app/components/shared/uploader.tsx with the following codes.

/* eslint-disable @next/next/no-img-element */
import { uploadFile } from '@/app/services/api.service'
import React, { useState } from 'react'
import { AiOutlineClose } from 'react-icons/ai'
import { FaCloudUploadAlt } from 'react-icons/fa'
import { LuPlus } from 'react-icons/lu'
import { toast } from 'react-toastify'
interface ComponentProps {
name: 'Poster' | 'Video'
type: string // Ensure this accepts any valid MIME type string
size: number
onUploadSuccess: (response: any) => void
onFileSelected: (file: any) => void
}
const Uploader: React.FC<ComponentProps> = ({
type,
size,
name,
onUploadSuccess,
onFileSelected,
}) => {
const [uploading, setUploading] = useState<boolean>(false)
const [progress, setProgress] = useState(0)
const [files, setFiles] = useState<File[]>([])
const removeFile = () => {
setFiles([])
}
const handleUploadProgress = (progressEvent: any) => {
const { loaded, total } = progressEvent
const percentCompleted = Math.round((loaded / Number(total)) * 100)
console.log(`Upload progress: ${percentCompleted}%`)
setProgress(percentCompleted)
}
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
}
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
}
const handleDrop = (event: React.DragEvent) => {
event.preventDefault()
const file = event.dataTransfer.files[0]
// Check file size
if (file.size > size * 1024 * 1024) {
alert(`File size must be less than ${size}MB.`)
return
}
// Dynamically create regex for filetype validation
const fileTypeRegex = createFileTypeRegex(type.split(', '))
// Check file type
if (!fileTypeRegex.test(file.type)) {
alert(`Only ${type} files are allowed.`)
return
}
onFileSelected(file)
setFiles([file])
}
const createFileTypeRegex = (fileTypes: string[]): RegExp => {
// Join the file types with the '|' character to indicate OR in regex
const joinedFileTypes = fileTypes.join('|')
return new RegExp(`^${joinedFileTypes}$`)
}
const handleClickOpenFileExplorer = () => {
// Create a hidden file input element
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.accept = type // Limit file types to MP4
fileInput.style.display = 'none' // Hide the file input
const handleFileSelection = (event: Event) => {
// Use event.currentTarget to access the input element
const target = event.currentTarget as HTMLInputElement
const file = target.files?.[0] // Now safely accessing the files property
if (!file) return
// Check file size
if (file.size > size * 1024 * 1024) {
// 150 MB in bytes
alert(`File size must be less than ${size}MB.`)
return
}
// Dynamically create regex for filetype validation
const fileTypeRegex = createFileTypeRegex(type.split(', '))
// Check file type
if (!fileTypeRegex.test(file.type)) {
alert(`Only ${type} files are allowed.`)
return
}
onFileSelected(file)
setFiles([file])
}
// Attach the modified event listener
fileInput.onchange = handleFileSelection
// Append the file input to the body temporarily
document.body.appendChild(fileInput)
fileInput.click() // Open the file dialog
// Remove the file input after the dialog is closed
fileInput.addEventListener('change', () => {
document.body.removeChild(fileInput)
})
}
const handleUpload = async () => {
setUploading(true)
await toast.promise(
new Promise<void>(async (resolve, reject) => {
await uploadFile(files[0], handleUploadProgress)
.then((res) => {
onUploadSuccess(res)
console.log(res)
resolve(res)
})
.catch((error) => reject(error))
}),
{
pending: 'Uploading...',
success: 'File successfully uploaded 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<>
{files.length < 1 && (
<div
className="flex flex-col items-center border-dashed border-2
border-slate-500 w-full h-32 justify-center rounded-xl
cursor-pointer hover:border-green-500"
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={handleClickOpenFileExplorer}
>
<LuPlus className="text-3xl" />
<p>Upload {name}</p>
<p className="text-sm text-slate-400">{size}mb Max</p>
</div>
)}
{files.length > 0 && (
<div className="flex flex-col justify-center shadow-md w-full h-32 rounded-lg overflow-hidden">
<div className="relative w-full h-full">
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-black opacity-75"></div>
<div className="relative h-full">
{type.includes('video') ? (
<video
className="w-full h-full object-cover"
src={URL.createObjectURL(files[0])}
/>
) : (
<img
className="w-full h-full object-cover"
src={URL.createObjectURL(files[0])}
alt="photo"
/>
)}
{uploading ? (
<>
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white text-green-600 rounded-full p-1 py-1.5">
<span className="text-sm h-6 w-6">{progress}%</span>
</div>
<div className="absolute bottom-0 left-0 right-0 bg-gray-700 rounded-full h-[2px]">
<span
className={`bg-green-500 h-full w-[${progress}%] flex`}
></span>
</div>
</>
) : (
<>
<button
onClick={handleUpload}
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2
bg-white text-green-600 rounded-full p-1.5 py-1.5
hover:bg-black hover:bg-opacity-70"
>
<FaCloudUploadAlt size={30} />
</button>
<button
className="absolute top-1 right-1 text-white bg-black bg-opacity-70
hover:text-red-500 rounded-full p-1"
onClick={removeFile}
>
<AiOutlineClose />
</button>
</>
)}
</div>
</div>
</div>
)}
</>
)
}
export default Uploader
view raw Uploader.tsx hosted with ❤ by GitHub

The changes made to this component are:

  1. Upload File Functionality: The original code didn't have the actual upload file functionality implemented. It only showed a success toast notification without uploading the file. This updated code includes the uploadFile function from api.service which handles the file upload.
  2. Progress Tracking: The updated code tracks the upload progress and displays it on the UI.
  3. Error Handling: The updated code includes error handling for the file upload process.
  4. File Type Validation: The updated code uses a more robust file type validation using a regular expression.
  5. Code Organization: The updated code is better organized, with separate functions for handling different tasks, making it easier to read and maintain.
  6. UI Updates: The updated code includes some UI updates, such as showing the upload progress and a cancel button during upload.

This updated code is more complete and robust, with actual file upload functionality, progress tracking, error handling, and better code organization.

The video below explains what each component does in more detail, kindly check it out for your betterment.

And that is it guys, we have completed this project, and the last step we need to take is to launch this project on the browser. Run $ yarn build && yarn start to see the project live on the browser.

If you encounter any issues, refer to the following resources for troubleshooting, till next time, all the best!

About Author

I am a web3 developer and the founder of Dapp Mentors, a company that helps businesses and individuals build and launch decentralized applications. I have over 8 years of experience in the software industry, and I am passionate about using blockchain technology to create new and innovative applications. I run a YouTube channel called Dapp Mentors where I share tutorials and tips on web3 development, and I regularly post articles online about the latest trends in the blockchain space.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more

Top comments (0)