Avatar Uploader with Server Actions

2024-04-12
By: O. Wolfson

Overview

This app provides a comprehensive system for managing user avatars within a Next.js application using Supabase as the backend. It enables users to log in, view their current avatar, upload a new image as an avatar. This system includes real-time UI updates, Supabase integration for data management, and image processing via the sharp library for resizing images before upload.

Find the code on Github

See the app implemented here

You can log in using test@owolf.com and password 123456.

AvatarUploader Component

javascript
"use client";
import { useTransition, useEffect, useState } from "react";
import {
  uploadImageToServer,
  logOutFromSupabase,
} from "@/actions/serverActions";
import { createClient } from "@/utils/supabase/client";
import Link from "next/link";
import Image from "next/image";

export default function AvatarUploader() {
  let [isPending, startTransition] = useTransition(); // React's concurrent mode API to manage transitions
  const [loggingOut, setLoggingOut] = useState(false); // State to handle logging out UI changes
  const [imageUrl, setImageUrl] = (useState < string) | (null > null); // State to store the URL of the uploaded image
  const [user, setUser] = useState < any > null; // State to store user information

  // useEffect to perform side effects only when isPending changes
  useEffect(() => {
    if (isPending) return; // Skip fetching if a transition is in progress

    const supabase = createClient(); // Initialize Supabase client

    // Function to fetch the avatar image from Supabase
    const getAvatarImageFromSupabase = async () => {
      const {
        data: { user },
      } = await supabase.auth.getUser(); // Get the currently authenticated user
      console.log("user", user?.id);
      setUser(user); // Set user data

      const { data: profile } = await supabase
        .from("profiles")
        .select()
        .eq("id", user?.id)
        .single(); // Query for the user profile

      setImageUrl(profile?.avatar_url); // Set image URL from user profile
    };

    getAvatarImageFromSupabase();
  }, [isPending]);

  // Handler for uploading images
  const uploadAction = async (formData: FormData) => {
    startTransition(() => {
      uploadImageToServer(formData);
    });
  };

  // Handler for logging out
  const logOutAction = async () => {
    await logOutFromSupabase();
  };

  // Render the component UI
  return (
    <main className="flex min-h-screen flex-col items-center justify-between py-12">
      <div className="z-10 max-w-xl w-full items-center justify-between flex flex-col gap-4 font-mono">
        <h1 className="text-4xl font-bold text-center">Image Uploader</h1>
        <p className="text-center">
          This is an uploader for images built with Next.js and Supabase.
        </p>

        {user ? (
          <div className="flex flex-col gap-2">
            <p className="font-bold">User: {user.email}</p>
            <form action={logOutAction} className="flex justify-center">
              <button
                className="border border-black rounded py-1 px-2 hover:bg-gray-300"
                type="submit"
                onClick={() => setLoggingOut(true)}
              >
                {loggingOut ? <p>Logging Out...</p> : <p>Log Out</p>}
              </button>
            </form>
          </div>
        ) : (
          <div>
            <p className="text-center">
              You must{" "}
              <Link className="underline font-bold" href="/login">
                log in
              </Link>{" "}
              to use this app.
            </p>
          </div>
        )}
        <form action={uploadAction} className="flex flex-col gap-4 pt-4">
          <input type="file" name="file" />
          <button
            type="submit"
            className="border border-black rounded py-1 px-2 hover:bg-gray-300"
            onClick={() => {
              setImageUrl(null);
            }}
          >
            {isPending ? "uploading..." : "upload"}{" "}
          </button>
        </form>
        <div className=" w-80 h-80 border border-black rounded relative">
          {imageUrl ? (
            <Image
              src={imageUrl}
              alt="avatar"
              fill={true}
              style={{ objectFit: "cover" }}
            />
          ) : (
            <div className="w-full h-full bg-gray-100 flex flex-col justify-center">
              <p className="text-center p-8 text-black">
                {user ? (
                  <span>Avatar Image</span>
                ) : (
                  <span>You need to be logged in to see your avatar</span>
                )}
              </p>
            </div>
          )}
        </div>
      </div>
    </main>
  );
}

Functionality

  1. State Management:

    • isPending: Manages the state of asynchronous actions, particularly during the image upload process.
    • loggingOut: Indicates whether a logout process is underway.
    • imageUrl: Stores the URL of the user's avatar image.
    • user: Contains user data fetched from Supabase.
  2. Effects and Lifecycle:

    • An effect hook triggers upon component mount and whenever isPending changes, fetching user details and their avatar URL from Supabase.
  3. User Interaction:

    • Image Upload: Users can upload a new avatar image through a file input. The upload initiates in a suspended transition state to optimize UI responsiveness.
    • Logging Out: Users can log out, which triggers an asynchronous request to Supabase to terminate the session.
  4. Rendering:

    • Displays the user's current avatar if available.
    • Provides a login link if the user is not authenticated.
    • Shows a button to start the image upload and another to log out.

Key Components

  • Link from next/link: Used for navigation to the login page if the user is not logged in.
  • Image from next/image: Optimizes image display.

Server Actions

uploadImageToServer(formData: FormData)

javascript
"use server";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
import sharp from "sharp";

const IMAGE_SIZE = 600;  // Define the target size of the resized image
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;  // Supabase URL from environment variables

// Function to upload an image to Supabase storage
export const uploadImageToServer = async (formData: FormData) => {
  const supabase = createClient();  // Initialize the Supabase client

  try {
    // Get the current authenticated user
    const {
      data: { user },
    } = await supabase.auth.getUser();
    if (!user) throw new Error("Authentication failed");  // If no user, throw an error

    const file = formData.get("file") as File;  // Extract the file from the FormData
    const resizedImage = await resizeImage(file);  // Resize the image using the sharp library

    // Construct the file path where the image will be stored
    const fileName = file.name.split(".")[0];
    const timestamp = Math.floor(Date.now() / 1000);
    const filePath = `${user.id}/${timestamp}_${fileName}.jpg`;

    // Upload the resized image to Supabase storage
    const { data, error } = await supabase.storage
      .from("uploads")
      .upload(filePath, resizedImage);

    if (error) throw error;  // If there's an upload error, throw it

    // Construct the URL to access the uploaded image
    const avatarUrl = `${SUPABASE_URL}/storage/v1/object/public/uploads/${data.path}`;
    await updateUserProfile(user.id, avatarUrl, supabase);  // Update the user profile with the new avatar URL
  } catch (error) {
    console.error("Failed to upload image:", error);
  }
};
Description

Processes the image upload request:

  1. Authenticates the user.
  2. Resizes the image to a predefined size using sharp.
  3. Uploads the resized image to Supabase Storage.
  4. Updates the user's profile with the new avatar URL.
Error Handling

Catches and logs errors related to user authentication and file upload processes.

resizeImage(file: File): Promise<Buffer>

javascript
// Function to resize an image to a square format and convert it to JPEG
const resizeImage = async (file: File): Promise<Buffer> => {
  const buffer = await file.arrayBuffer(); // Convert file to array buffer
  return sharp(buffer)
    .resize(IMAGE_SIZE, IMAGE_SIZE) // Resize the image
    .toFormat("jpeg") // Convert the image to JPEG format
    .toBuffer(); // Return the modified image as a buffer
};
Description

Converts a file from FormData into a Buffer and resizes it to a square format (600x600 pixels), changing the format to JPEG.

updateUserProfile(userId: string, avatarUrl: string, supabase: any)

javascript
// Function to update the user profile with a new avatar URL
const updateUserProfile = async (
  userId: string,
  avatarUrl: string,
  supabase: any
) => {
  const { error } = await supabase
    .from("profiles")
    .update({ avatar_url: avatarUrl })
    .eq("id", userId); // Update the 'profiles' table where 'id' matches userId

  if (error) throw error; // If there's an update error, throw it
};
Description

Updates the user's profile in the Supabase database with the new avatar URL.

Error Handling

Checks for errors during the profile update and logs them.

logOutFromSupabase()

javascript
// Function to log out from Supabase
export const logOutFromSupabase = async () => {
  const supabase = createClient(); // Initialize the Supabase client

  try {
    const { error } = await supabase.auth.signOut(); // Attempt to sign out
    if (error) {
      throw new Error(`Logout failed: ${error.message}`); // If logout fails, throw an error
    }
  } catch (error) {
    console.error("Logout error:", error);
  } finally {
    redirect("/login"); // Redirect to login page after logout
  }
};
Description

Logs out the current user by ending the session in Supabase and redirects the user to the login page.

Error Handling

Handles errors that might occur during the logout process and logs them, ensuring the user is redirected irrespective of whether the logout was successful.

Integration with Supabase

  • Authentication: Uses Supabase's authentication mechanisms to manage user sessions.
  • Storage: Utilizes Supabase Storage for storing and retrieving user avatars.
  • Realtime Database: Interacts with the Supabase realtime database for fetching and updating user profiles.

Usage

This component and its server functions are designed to be used in applications requiring user profile management with image uploads. It is suitable for environments where user experience and efficient data handling are priorities.