Back to roadmaps supabase Course

Project: User Profile Avatar System

In this project, we will build a user profile avatar system. Users can select an image file, upload it to a public storage bucket, and save the public image link to their database profile.


1. Relational Database Tables

We store profiles in a database table containing a matching user ID reference:

-- Profiles table setup referencing auth.users UUID primary key
CREATE TABLE profiles (
  id UUID REFERENCES auth.users PRIMARY KEY,
  username TEXT NOT NULL,
  avatar_url TEXT
);

2. Implementing the Avatar Component

Here is the React user dashboard avatar component:

// src/components/AvatarUpload.tsx
import React, { useState, useEffect } from "react";
import { supabase } from "../lib/supabase";

export default function AvatarUpload() {
  const [loading, setLoading] = useState(false);
  const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
  const [username, setUsername] = useState("");

  useEffect(() => {
    loadProfileDetails();
  }, []);

  async function loadProfileDetails() {
    const { data: { user } } = await supabase.auth.getUser();
    if (!user) return;

    const { data } = await supabase
      .from("profiles")
      .select("username, avatar_url")
      .eq("id", user.id)
      .single();

    if (data) {
      setUsername(data.username);
      setAvatarUrl(data.avatar_url);
    }
  }

  async function handleFileUpload(event: React.ChangeEvent<HTMLInputElement>) {
    try {
      setLoading(true);
      const { data: { user } } = await supabase.auth.getUser();
      
      if (!user || !event.target.files || event.target.files.length === 0) {
        throw new Error("Invalid file upload target session");
      }

      const file = event.target.files[0];
      const fileExt = file.name.split(".").pop();
      const filePath = `${user.id}/avatar.${fileExt}`;

      // 1. Upload the image file to the avatars storage bucket
      const { error: uploadError } = await supabase.storage
        .from("avatars")
        .upload(filePath, file, { upsert: true });

      if (uploadError) throw uploadError;

      // 2. Retrieve the public URL
      const { data: { publicUrl } } = supabase.storage
        .from("avatars")
        .getPublicUrl(filePath);

      // 3. Save the public URL to the database profiles table
      const { error: dbError } = await supabase
        .from("profiles")
        .upsert({
          id: user.id,
          username,
          avatar_url: publicUrl,
        });

      if (dbError) throw dbError;

      setAvatarUrl(publicUrl);
      alert("Avatar updated successfully!");
    } catch (err: any) {
      alert(err.message || "Failed to update avatar photo");
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className="flex flex-col items-center gap-6 p-8 border rounded-2xl max-w-sm mx-auto bg-white shadow-sm mt-10">
      <div className="relative w-32 h-32 rounded-full overflow-hidden border bg-gray-50 flex items-center justify-center">
        {avatarUrl ? (
          <img src={avatarUrl} alt="User Profile" className="w-full h-full object-cover" />
        ) : (
          <span className="text-gray-400 text-sm">No Photo</span>
        )}
      </div>

      <div className="text-center">
        <h4 className="font-bold text-gray-950">{username || "User Profile"}</h4>
        <p className="text-gray-400 text-xs mt-1">Accepts PNG, JPG formats</p>
      </div>

      <label className="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-5 rounded-lg text-sm cursor-pointer transition">
        {loading ? "Uploading..." : "Upload Avatar"}
        <input
          type="file"
          accept="image/*"
          onChange={handleFileUpload}
          disabled={loading}
          className="hidden"
        />
      </label>
    </div>
  );
}
Published on Last updated: