April 12, 2024
O. Wolfson
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.
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>
            
              You must{" "}
              
                log in
              {" "}
              to use this app.
            
          
        )}
        
          
           {
              setImageUrl(null);
            }}
          >
            {isPending ? "uploading..." : "upload"}{" "}
          
        
        
          {imageUrl ? (
            
          ) : (
            
              
                {user ? (
                  Avatar Image
                ) : (
                  You need to be logged in to see your avatar
                )}
              
            
          )}
        
      
    
  );
}
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.Effects and Lifecycle:
isPending changes, fetching user details and their avatar URL from Supabase.User Interaction:
Rendering:
next/link: Used for navigation to the login page if the user is not logged in.next/image: Optimizes image display.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
     fileName = file..()[];
     timestamp = .(.() / );
     filePath = ;
    
     { data, error } =  supabase.
      .()
      .(filePath, resizedImage);
     (error)  error;  
    
     avatarUrl = ;
     (user., avatarUrl, supabase);  
  }  (error) {
    .(, error);
  }
};
Processes the image upload request:
sharp.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
};
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
};
Updates the user's profile in the Supabase database with the new avatar URL.
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
  }
};
Logs out the current user by ending the session in Supabase and redirects the user to the login page.
Handles errors that might occur during the logout process and logs them, ensuring the user is redirected irrespective of whether the logout was successful.
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.