Epic Next JS 15 Tutorial Part 5: File upload using server actions
In this tutorial, we will create a Dashboard layout with an Account section where users can update their first name, last name, bio, and image using Next.js and Strapi. We will also handle file uploads using NextJs server actions. First, let's set up our project by installing the necessary dependencies: ```bash npx create-next-app --ts cd your-project-name npm install axios js-cookie strapi-sdk ``` Next, we will create a new file named `_document.js` in the pages directory and paste the following code to customize our Next.js application: ```javascript import Document, { Html, Head, Main, NextScript } from 'next/document'; class MyDocument extends Document { static async getInitialProps(ctx) { const initialProps = await Document.getInitialProps(ctx); return { ...initialProps }; } render() { return ( <Html lang="en"> <Head /> <body> <Main /> <NextScript /> </body> </Html> ); } } export default MyDocument; ``` Now, let's create a new file named `_app.js` in the pages directory and paste the following code to set up our custom auth check: ```javascript import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import Cookies from 'js-cookie'; import axios from 'axios'; function MyApp({ Component, pageProps }) { const router = useRouter(); const [loading, setLoading] = useState(true); useEffect(() => { const handleStart = () => setLoading(true); const handleComplete = () => setLoading(false); router.events.on('routeChangeStart', handleStart); router.events.on('routeChangeComplete', handleComplete); router.events.on('routeChangeError', handleComplete); return () => { router.events.off('routeChangeStart', handleStart); router.events.off('routeChangeComplete', handleComplete); router.events.off('routeChangeError', handleComplete); }; }, [router]); useEffect(() => { const jwt = Cookies.get('jwt'); if (!loading && !jwt) { router.push('/login'); } }, [loading, router]); return loading ? ( <div>Loading...</div> ) : ( <Component {...pageProps} /> ); } export default MyApp; ``` Now that we have our project set up let's create a new file named `login.js` in the pages directory and paste the following code to handle user login: ```javascript import React, { useState } from 'react'; import axios from 'axios'; import Router from 'next/router'; import Cookies from 'js-cookie'; const Login = () => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const handleSubmit = async (e) => { e.preventDefault(); try { const response = await axios.post('/api/auth', { email, password }); Cookies.set('jwt', response.data.token); Router.push('/dashboard'); } catch (error) { console.error(error); } }; return ( <div> <form onSubmit={handleSubmit}> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} /> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} /> <button type="submit">Login</button> </form> </div> ); }; export default Login; ``` Next, let's create a new file named `dashboard.js` in the pages directory and paste the following code to handle user dashboard: ```javascript import React from 'react'; import { useState } from 'react'; import axios from 'axios'; import Router from 'next/router'; import Cookies from 'js-cookie'; const Dashboard = () => { const [user, setUser] = useState(null); useEffect(() => { const jwt = Cookies.get('jwt'); if (!jwt) { Router.push('/login'); } else { axios.get('/api/users/me', { headers: { Authorization: `Bearer ${jwt}` } }) .then((response) => setUser(response.data)) .catch((error) => console.error(error)); } }, []); return ( <div> {user ? ( <div> <h1>{user.firstName} {user.lastName}</h1> <p>{user.bio}</p> <img src={user.image.url} alt={user.image.alternativeText} /> </div> ) : ( <div>Loading...</div> )} </div> ); }; export default Dashboard; ``` Now, let's create a new file named `auth.js` in the pages directory and paste the following code to handle user authentication: ```javascript import axios from 'axios'; const auth = async (req) => { try { const response = await axios.post('/api/auth', req.body); return { success: true, data: response.data }; } catch (error) { console.error(error); return { success: false, error: 'Invalid email or password.' }; } }; export default auth; ``` Next, let's create a new file named `users.js` in the pages directory and paste the following code to handle user data: ```javascript import axios from 'axios'; const users = async (req) => { try { const response = await axios.get('/api/users/me', { headers: { Authorization: `Bearer ${req.headers.authorization}` } }); return { success: true, data: response.data }; } catch (error) { console.error(error); return { success: false, error: 'Failed to fetch user data.' }; } }; export default users; ``` Now that we have our backend set up let's create a new file named `get-user-me-loader.ts` in the services directory and paste the following code to handle user data loading: ```typescript import qs from 'qs'; import { getAuthToken } from './get-token'; import { getStrapiURL } from '../lib/utils'; export async function getUserMeLoader() { const baseUrl = getStrapiURL(); const url = new URL('/api/users/me', baseUrl); url.search = qs.stringify({ populate: 'image', }); const authToken = await getAuthToken(); if (!authToken) return { ok: false, data: null, error: null }; try { const response = await fetch(url.href, { method: 'GET', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authToken}`, }, }); const data = await response.json(); if (data.error) return { ok: false, data: null, error: data.error }; return { ok: true, data: data, error: null }; } catch (error) { console.log(error); return { ok: false, data: null, error: error }; } } ``` Now that we have our backend set up let's create a new file named `update-profile-action.ts` in the actions directory and paste the following code to handle user profile updates: ```typescript import { z } from 'zod'; import qs from 'qs'; import { revalidatePath } from 'next/cache'; import { getUserMeLoader } from '../services/get-user-me-loader'; import { mutateData } from '../services/mutate-data'; const profileSchema = z.object({ firstName: z.string().min(1, 'First name is required.'), lastName: z.string().min(1, 'Last name is required.'), bio: z.string(), }); export async function updateProfileAction( userId: string, prevState: any, formData: FormData ) { const rawFormData = Object.fromEntries(formData); const payload = { firstName: rawFormData.firstName, lastName: rawFormData.lastName, bio: rawFormData.bio, }; const responseData = await mutateData('PUT', `/api/users/${userId}`, payload); revalidatePath('/dashboard'); return { ...prevState, data: responseData, message: 'Profile updated.', }; } ``` Now that we have our backend set up let's create a new file named `update-image-action.ts` in the actions directory and paste the following code to handle user profile image updates: ```typescript import { z } from 'zod'; import qs from 'qs'; import { revalidatePath } from 'next/cache'; import { getUserMeLoader } from '../services/get-user-me-loader'; import { mutateData } from '../services/mutate-data'; import { fileDeleteService, fileUploadService } from '../services/file-service'; const imageSchema = z.object({ image: z .any() .refine((file) => { if (file.size === 0 || file.name === undefined) return false; else return true; }, 'Please update or add new image.') .refine( (file) => ['image/jpeg', 'image/jpg', 'image/png'].includes(file?.type), '.jpg, .jpeg, .png files are accepted.' ) .refine((file) => file.size <= 5000000, `Max file size is 5MB.`), }); export async function uploadProfileImageAction( imageId: string, prevState: any, formData: FormData ) { const user = await getUserMeLoader(); if (!user.ok) throw new Error('You are not authorized to perform this action.'); const userId = user.data.id; const data = Object.fromEntries(formData); const validatedFields = imageSchema.safeParse({ image: data.image, }); if (!validatedFields.success) { return { ...prevState, zodErrors: validatedFields.error.flatten().fieldErrors, strapiErrors: null, data: null, message: 'Invalid Image', }; } if (imageId) { try { await fileDeleteService(imageId); } catch (error) { return { ...prevState, strapiErrors: null, zodErrors: null, message: 'Failed to Delete Previous Image.', }; } } const fileUploadResponse = await fileUploadService(data.image); if (!fileUploadResponse) { return { ...prevState, strapiErrors: null, zodErrors: null, message: 'Ops! Something went wrong. Please try again.', }; } if (fileUploadResponse.error) { return { ...prevState, strapiErrors: fileUploadResponse.error, zodErrors: null, message: 'Failed to Upload File.', }; } const updatedImageId = fileUploadResponse[0].id; const payload = { image: updatedImageId }; const updateImageResponse = await mutateData( 'PUT', `/api/users/${userId}`, payload ); revalidatePath('/dashboard'); return { ...prevState, data: updateImageResponse, zodErrors: null, strapiErrors: null, message: 'Image Uploaded', }; } ``` Now that we have our backend set up let's create a new file named `file-service.ts` in the services directory and paste the following code to handle file uploads and deletions: ```typescript import { getAuthToken } from './get-token'; import { mutateData } from './mutate-data'; import { getStrapiURL } from '../lib/utils'; export async function fileDeleteService(imageId: string) { const authToken = await getAuthToken(); if (!authToken) throw new Error('No auth token found'); const data = await mutateData('DELETE', `/api/upload/files/${imageId}`); return data; } export async function fileUploadService(image: any) { const authToken = await getAuthToken(); if (!authToken) throw new Error('No auth token found'); const baseUrl = getStrapiURL(); const url = new URL('/api/upload', baseUrl); const formData = new FormData(); formData.append('files', image, image.name); try { const response = await fetch(url, { headers: { Authorization: `Bearer ${authToken}` }, method: 'POST', body: formData, }); const dataResponse = await response.json(); return dataResponse; } catch (error) { console.error('Error uploading image:', error); throw error; } } ``` Now that we have our backend set up let's create a new file named `update-profile-form.tsx` in the components directory and paste the following code to handle user profile updates: ```typescript import React, { useState } from 'react'; import { useRouter } from 'next/router'; import axios from 'axios'; import Router from 'next/router'; import Cookies from 'js-cookie'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { profileSchema } from '../actions/update-profile-action'; import { updateProfileAction } from '../actions/update-profile-action'; import { uploadProfileImageAction } from '../actions/update-image-action'; import { fileDeleteService, fileUploadService } from '../services/file-service'; const UpdateProfileForm = () => { const router = useRouter(); const userId = router.query.id as string; const [loading, setLoading] = useState(false); const [imageId, setImageId] = useState<string | null>(null); const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(profileSchema), }); const onSubmit = async (data) => { try { setLoading(true); if (imageId) { await fileDeleteService(imageId); } const responseData = await updateProfileAction(userId, {}, new FormData()); revalidatePath('/dashboard'); alert(responseData.message); } catch (error) { console.error(error); alert('Failed to update profile.'); } finally { setLoading(false); } }; return ( <form onSubmit={handleSubmit(onSubmit)}> <input type="text" placeholder="First Name" {...register('firstName')} /> {errors.firstName && <p>{errors.firstName.message}</p>} <input type="text" placeholder="Last Name" {...register('lastName')} /> {errors.lastName && <p>{errors.lastName.message}</p>} <textarea placeholder="Bio" {...register('bio')} /> <label htmlFor="image">Upload Image</label> <input type="file" id="image" onChange={(e) => setImageId(null)} /> <button type="submit" disabled={loading}>Update Profile</button> </form> ); }; export default UpdateProfileForm; ``` Now that we have our backend set up let's create a new file named `update-image-form.tsx` in the components directory and paste the following code to handle user profile image updates: ```typescript import React, { useState } from 'react'; import { useRouter } from 'next/router'; import axios from 'axios'; import Router from 'next/router'; import Cookies from 'js-cookie'; import { zodResolver } from '@hookform/resolvers/zod'; import { imageSchema } from '../actions/update-image-action'; import { uploadProfileImageAction } from '../actions/update-image-action'; import { fileDeleteService, fileUploadService } from '../services/file-service'; const UpdateImageForm = () => { const router = useRouter(); const userId = router.query.id as string; const [loading, setLoading] = useState(false); const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(imageSchema), }); const onSubmit = async (data) => { try { setLoading(true); const responseData = await uploadProfileImageAction(null, {}, new FormData()); revalidatePath('/dashboard'); alert(responseData.message); } catch (error) { console.error(error); alert('Failed to update image.'); } finally { setLoading(false); } }; return ( <form onSubmit={handleSubmit(onSubmit)}> <label htmlFor="image">Upload Image</label> <input type="file" id="image" onChange={(e) => setImageId(null)} /> {errors.image && <p>{errors.image.message}</p>} <button type="submit" disabled={loading}>Update Image</button> </form> ); }; export default UpdateImageForm; ``` Now that we have our backend set up let's create a new file named `account.js` in the pages directory and paste the following code to handle user account data: ```javascript import React, { useState } from 'react'; import axios from 'axios'; import Router from 'next/router'; import Cookies from 'js-cookie'; import UpdateProfileForm from '../components/update-profile-form'; import UpdateImageForm from '../components/update-image-form'; const Account = () => { const [user, setUser] = useState(null); useEffect(() => { const jwt = Cookies.get('jwt'); if (!jwt) { Router.push('/login'); } else { axios.get('/api/users/me', { headers: { Authorization: `Bearer ${jwt}` } }) .then((response) => setUser(response.data)) .catch((error) => console.error(error)); } }, []); return ( <div> {user ? ( <div> <h1>{user.firstName} {user.lastName}</h1> <p>{user.bio}</p> <img src={user.image.url} alt={user.image.alternativeText} /> <UpdateProfileForm userId={user.id} /> <UpdateImageForm userId={user.id} /> </div> ) : ( <div>Loading...</div> )} </div> ); }; export default Account; ``` Now that we have our backend set up let's create a new file named `account.js` in the pages directory and paste the following code to handle user account data: ```javascript import React, { useState } from 'react'; import axios from 'axios'; import Router from 'next/router'; import Cookies from 'js-cookie'; import UpdateProfileForm from '../components/update-profile-form'; import UpdateImageForm from '../components/update-image-form'; const Account = () => { const [user, setUser] = useState(null); useEffect(() => { const jwt = Cookies.get('jwt'); if (!jwt) { Router.push('/login'); } else { axios.get('/api/users/me', { headers: { Authorization: `Bearer ${jwt}` } }) .then((response) => setUser(response.data)) .catch((error) => console.error(error)); } }, []); return ( <div> {user ? ( <div> <h1>{user.firstName} {user.lastName}</h1> <p>{user.bio}</p> <img src={user.image.url} alt={user.image.alternativeText} /> <UpdateProfileForm userId={user.id} /> <UpdateImageForm userId={user.id} /> </div> ) : ( <div>Loading...</div> )} </div> ); }; export default Account; ``` Now that we have our backend set up let's create a new file named `account.js` in the pages directory and paste the following code to handle user account data: ```javascript import React, { useState } from 'react'; import axios from 'axios'; import Router from 'next/router'; import Cookies from 'js-cookie'; import UpdateProfileForm from '../components/update-profile-form'; import UpdateImageForm from '../components/update-image-form'; const Account = () => { const [user, setUser] = useState(null); useEffect(() => { const jwt = Cookies.get('jwt'); if (!jwt) { Router.push('/login'); } else { axios.get('/api/users/me', { headers: { Authorization: `Bearer ${jwt}` } }) .then((response) => setUser(response.data)) .catch((error) => console.error(error)); } }, []); return ( <div> {user ? ( <div> <h1>{user.firstName} {user.lastName}</h1> <p>{user.bio}</p> <img src={user.image.url} alt={user.image.alternativeText} /> <UpdateProfileForm userId={user.id} /> <UpdateImageForm userId={user.id} /> </div> ) : ( <div>Loading...</div> )} </div> ); }; export default Account; ```
Company
Strapi
Date published
April 3, 2024
Author(s)
Paul Bratslavsky
Word count
6239
Language
English
Hacker News points
None found.