Project Rongtuli- A Social Media App
Profile Page Set Up: Bio and Image

এই সেকশনে আমাদের কাজ, ,

১। ছবির উপরে ইডিট পেনে ক্লিক করে ছবি পরিবর্তন ২। বায়ো সেকশনে ইডিট পেনে ক্লিক করে, বায়ো টেক্সট আপডেট

স্টেট ভেরিয়েবলগুলো হলো প্রাইভেট ডাটা।

এই পেজের স্টেট ভেরিয়েবল গুলো আলাদা জায়গায় মেইনটেইন করলে, আমরা আলাদা একটা সুবিধা পাবো।

১। এই পেজে আমাদের অনেক একশন হ্যান্ডেল করতে হবে। যদি কম্পোনেন্টে হ্যান্ডেল করি। তাহলে কোড অনেক বেড়ে যাবে। আমরা reducer function লিখে সব একশনকে হ্যান্ডেল করতে পারি, কম্পোনেন্টের বাইরে। যেটি একটি ইনিশিয়াল স্টেট নেয়, একটি নতুন স্টেট রিটার্ণ করে। আগের স্টেটের প্রোপার্টি এবং নতুন স্টেটের প্রোপার্টি একই থাকবে।

২। আমরা প্রোফাইল পেজে ইমেজ চেঞ্জ করলে হেডারেও সিংক হয়ে যাবে। তার মানে প্রোফাইল ডাটা কোনো Context এ রাখতে হবে। যখন আমরা প্রোজেক্ট করছি, তখন আমাদের ডেটা মডেলিং নিয়ে ভাবতে হবে। এখানে Auth এর মধ্যে ছবি থাকা সত্তেও, সেটি প্রোফাইল পেজে ব্যবহার করব না। Auth Data authentication এর জন্য, প্রোফাইলের জন্য নয়। Auth Model থেকে data নিয়ে হেডারে রেখেছি। সেটা আমরা করব না। Profile এর জন্য আমরা Profile Context এবং Profile Provider তইরি করব। Context এবং Provider এর মধ্যে reducer দিয়ে ডেটা পাস করব।

এখন আমরা যদি প্রোফাইলের কোনো ডাটা চেঞ্জ করি। তখন আমাদের Auth এর কোনো ডাটা চেঞ্জ করা লাগবে না।


তাহলে আমরা বুঝে গেলাম আমাদের কি কি দরকার? আমাদের ১ নাম্বার দরকার- প্রোফাইলে কি কি একশন আছে। ২। Profiel reducer ৩। Profile Context ৪। Profiel Provider 5। তারপর আমরা একটা useProfile() নামে একটা হুক থাকবে। যার মাধ্যেমে, ডাটাগুলো আমাদের থ্রো আউট দ্যা আপ্লিকেশন এভেইলএবল রাখবে।

এবং আমরা চাইলে প্রোফাইলের ইনফরমেশনগুলো ইডিটও করতে পারবো।


1. Define Profile Actions

প্রথমে প্রোফাইল পেজের বিভিন্ন একশন (যেমন: ডেটা ফেচ করা, আপডেট করা, ত্রুটি ইত্যাদি) ডিফাইন করব।

// actions/index.js
export const actions = {
  profile: {
    PROFILE_FETCHING: "PROFILE_FETCHING",
    PROFILE_FETCHED: "PROFILE_FETCHED",
    PROFILE_FETCH_ERROR: "PROFILE_FETCH_ERROR",
    PROFILE_UPDATED: "PROFILE_UPDATED",
    IMAGE_UPDATED: "IMAGE_UPDATED",
  },
};

2. Create Profile Reducer

profileReducer স্টেট আপডেট করার জন্য ব্যবহৃত হবে। প্রতিটি একশনের জন্য স্টেট কীভাবে পরিবর্তন হবে তা এখানে নির্ধারিত।

// reducers/profileReducer.js
const initialState = {
  user: null,
  posts: [],
  loading: false,
  error: null,
};
 
const profileReducer = (state, action) => {
  switch (action.type) {
    case actions.profile.PROFILE_FETCHING:
      return { ...state, loading: true, error: null };
    case actions.profile.PROFILE_FETCHED:
      return {
        ...state,
        user: action.data.user,
        posts: action.data.posts,
        loading: false,
      };
    case actions.profile.PROFILE_FETCH_ERROR:
      return { ...state, error: action.error, loading: false };
    case actions.profile.PROFILE_UPDATED:
      return { ...state, user: action.data, loading: false };
    case actions.profile.IMAGE_UPDATED:
      return {
        ...state,
        user: { ...state.user, avatar: action.data.avatar },
        loading: false,
      };
    default:
      return state;
  }
};
 
export { initialState, profileReducer };

3. Create Profile Context and Provider

Context এবং Provider ব্যবহার করে প্রোফাইল ডেটা পুরো অ্যাপ জুড়ে শেয়ার করা সহজ হবে।

// context/ProfileContext.js
import React, { createContext, useContext, useReducer } from "react";
import { profileReducer, initialState } from "../reducers/profileReducer";
 
const ProfileContext = createContext();
 
export const ProfileProvider = ({ children }) => {
  const [state, dispatch] = useReducer(profileReducer, initialState);
 
  return (
    <ProfileContext.Provider value={{ state, dispatch }}>
      {children}
    </ProfileContext.Provider>
  );
};
 
export const useProfile = () => useContext(ProfileContext);

4. Refactor ProfilePage

ProfilePage-এ আমরা Context থেকে ডেটা নেব এবং dispatch ব্যবহার করব একশন হ্যান্ডল করতে।

import { useEffect } from "react";
import { actions } from "../actions";
import { useProfile } from "../context/ProfileContext";
import { useAuth } from "../hooks/useAuth";
import useAxios from "../hooks/useAxios";
import ProfileInfo from "../components/Profile/ProfileInfo";
import Posts from "../components/Profile/MyPosts";
 
const ProfilePage = () => {
  const { state, dispatch } = useProfile();
  const { auth } = useAuth();
  const { api } = useAxios();
 
  useEffect(() => {
    dispatch({ type: actions.profile.PROFILE_FETCHING });
    const fetchProfile = async () => {
      try {
        const response = await api.get(`/profile/${auth.user.id}`);
        dispatch({
          type: actions.profile.PROFILE_FETCHED,
          data: response.data,
        });
      } catch (error) {
        dispatch({
          type: actions.profile.PROFILE_FETCH_ERROR,
          error: error.message,
        });
      }
    };
 
    fetchProfile();
  }, [auth.user.id, api]);
 
  if (state.loading) return <div>Loading...</div>;
  if (state.error) return <div>Error: {state.error}</div>;
 
  return (
    <div>
      <ProfileInfo />
      <Posts posts={state.posts} />
    </div>
  );
};
 
export default ProfilePage;

5. Implement Profile Image Upload

ProfileImage কম্পোনেন্ট ব্যবহার করে ছবি আপলোড করব। এখানে useRef দিয়ে ফাইল সিলেক্টর হ্যান্ডল করা হয়েছে।

import { useRef } from "react";
import useAxios from "../../hooks/useAxios";
import useProfile from "../../hooks/useProfile";
import { actions } from "./../../actions/index";
 
const ProfileImage = () => {
  const { state, dispatch } = useProfile();
  const { api } = useAxios();
  const fileUploaderRef = useRef();
 
  const handleImageUpload = (event) => {
    event.preventDefault();
 
    fileUploaderRef.current.addEventListener("change", updateImageDisplay);
    fileUploaderRef.current.click();
  };
 
  const updateImageDisplay = async () => {
    try {
      const formData = new FormData();
      for (const file of fileUploaderRef.current.files) {
        formData.append("avatar", file);
      }
 
      const response = await api.post(
        `${import.meta.env.VITE_SERVER_BASE_URL}/profile/${
          state?.user?.id
        }/avatar`,
        formData
      );
      if (response.status === 200) {
        dispatch({
          type: actions.profile.IMAGE_UPDATED,
          data: response.data,
        });
      }
    } catch (err) {
      dispatch({
        type: actions.profile.DATA_FETCH_ERROR,
        error: err.message,
      });
    }
  };
 
  return (
    <div className="relative mb-8 max-h-[180px] max-w-[180px] rounded-full lg:mb-11 lg:max-h-[218px] lg:max-w-[218px]">
      <img
        className="max-w-full rounded-full"
        src={`${import.meta.env.VITE_SERVER_BASE_URL}/${state?.user?.avatar}`}
        alt={state?.user?.firstName}
      />
 
      <form id="form" encType="multipart/form-data">
        <button
          className="flex-center absolute bottom-4 right-4 h-7 w-7 rounded-full bg-black/50 hover:bg-black/80"
          onClick={handleImageUpload}
          type="submit"
        >
          <img src="./assets/icons/edit.svg" alt="Edit" />
        </button>
        <input id="file" type="file" ref={fileUploaderRef} hidden />
      </form>
    </div>
  );
};
 
export default ProfileImage;

এই কোডটি একটি React কম্পোনেন্ট যা ব্যবহারকারীর প্রোফাইল ছবির আপলোড এবং আপডেট করার জন্য ব্যবহৃত হয়। আমি প্রত্যেকটি অংশ এবং কী-ওয়ার্ড ব্যাখ্যা করবো যেন একজন বিগিনারও সহজে বুঝতে পারে।


লাইন বাই লাইন ব্যাখ্যা:

avatar, ইউজার ছবির ফাইল পাত দেয়, (uploads/avatar/avatar-1733974637068-648383628.jpg) যা সার্ভারে সেভ থাকে। ব্রাউজার ইমেজ লোড করার জন্য সম্পূর্ণ URL চায়, যেখানে ডোমেইন বা সার্ভারের ঠিকানাসহ ফাইল পাথ থাকতে হবে।

ইম্পোর্ট অংশ

import { useRef } from "react";
import useAxios from "../../hooks/useAxios";
import useProfile from "../../hooks/useProfile";
import { actions } from "./../../actions/index";
  • useRef: এটি React এর একটি হুক যা DOM উপাদান বা ভেরিয়েবল রেফারেন্স করে রাখে। এখানে ফাইল আপলোড ইনপুট এর রেফারেন্স ধরে রাখার জন্য ব্যবহৃত হচ্ছে।
  • useAxios: এটি একটি কাস্টম হুক যা API কল করার জন্য ব্যবহৃত হয়। এটি কোডে পুনঃব্যবহারযোগ্য ফাংশন সরবরাহ করে।
  • useProfile: এটি আরেকটি কাস্টম হুক যা ব্যবহারকারীর প্রোফাইল সম্পর্কিত ডেটা এবং অ্যাকশন অ্যাক্সেস করতে ব্যবহৃত হয়।
  • actions: এটি একটি অবজেক্ট যা Redux-এর মতো স্টেট ম্যানেজমেন্টে বিভিন্ন অ্যাকশন টাইপ সংজ্ঞায়িত করতে ব্যবহৃত হয়।

ProfileImage কম্পোনেন্ট ডিক্লারেশন

const ProfileImage = () => {
  const { state, dispatch } = useProfile();
  const { api } = useAxios();
  const fileUploaderRef = useRef();
  • ProfileImage: একটি ফাংশনাল কম্পোনেন্ট যা প্রোফাইল ইমেজ আপডেট করার জন্য তৈরি।
  • state, dispatch: useProfile থেকে স্টেট (বর্তমান ডেটা) এবং dispatch (স্টেট পরিবর্তনের জন্য ব্যবহৃত) পাওয়া যাচ্ছে।
  • api: useAxios থেকে API কল করার জন্য api ফাংশন পাওয়া যাচ্ছে।
  • fileUploaderRef: useRef ব্যবহার করে ফাইল ইনপুট এলিমেন্টের রেফারেন্স রাখা হচ্ছে।

ইমেজ আপলোড হ্যান্ডলিং ফাংশন

const handleImageUpload = (event) => {
  event.preventDefault();
 
  fileUploaderRef.current.addEventListener("change", updateImageDisplay);
  fileUploaderRef.current.click();
};
  • handleImageUpload: একটি ইভেন্ট হ্যান্ডলার যা ফাইল আপলোড ডায়ালগ বক্স খুলে।
  • event.preventDefault(): পেজ রিলোড হওয়া বন্ধ করার জন্য।
  • fileUploaderRef.current.addEventListener("change", updateImageDisplay): যখন ব্যবহারকারী নতুন ফাইল নির্বাচন করবে, তখন updateImageDisplay ফাংশন চালু হবে।
  • fileUploaderRef.current.click(): জাভাস্ক্রিপ্ট দিয়ে ফাইল আপলোড ডায়ালগ বক্স প্রোগ্রামেটিক্যালি খুলে।

ইমেজ প্রসেসিং এবং সার্ভারে পাঠানো

const updateImageDisplay = async () => {
  try {
    const formData = new FormData();
    for (const file of fileUploaderRef.current.files) {
      formData.append("avatar", file);
    }
 
    const response = await api.post(
      `${import.meta.env.VITE_SERVER_BASE_URL}/profile/${
        state?.user?.id
      }/avatar`,
      formData
    );
    if (response.status === 200) {
      dispatch({
        type: actions.profile.IMAGE_UPDATED,
        data: response.data,
      });
    }
  } catch (err) {
    dispatch({
      type: actions.profile.DATA_FETCH_ERROR,
      error: err.message,
    });
  }
};
  • updateImageDisplay: এটি একটি অ্যাসিঙ্ক ফাংশন যা ফাইল আপলোড করে।
  • new FormData(): এটি ফাইল এবং ডেটা প্রক্রিয়া করার জন্য HTML ফর্ম ডেটা তৈরি করে।
  • fileUploaderRef.current.files: ব্যবহারকারীর নির্বাচিত ফাইলগুলোর অ্যারে।
  • formData.append("avatar", file): ফাইল সার্ভারে পাঠানোর জন্য ফর্ম ডেটাতে যোগ করা হচ্ছে।
  • api.post: API কল করে প্রোফাইল ইমেজ সার্ভারে আপলোড করা হচ্ছে।
  • dispatch: সফল হলে IMAGE_UPDATED অ্যাকশন প্রেরণ করে। ব্যর্থ হলে DATA_FETCH_ERROR অ্যাকশন প্রেরণ করে।

UI তৈরি

return (
  <div className="relative mb-8 max-h-[180px] max-w-[180px] rounded-full lg:mb-11 lg:max-h-[218px] lg:max-w-[218px]">
    <img
      className="max-w-full rounded-full"
      src={`${import.meta.env.VITE_SERVER_BASE_URL}/${state?.user?.avatar}`}
      alt={state?.user?.firstName}
    />
 
    <form id="form" encType="multipart/form-data">
      <button
        className="flex-center absolute bottom-4 right-4 h-7 w-7 rounded-full bg-black/50 hover:bg-black/80"
        onClick={handleImageUpload}
        type="submit"
      >
        <img src="./assets/icons/edit.svg" alt="Edit" />
      </button>
      <input id="file" type="file" ref={fileUploaderRef} hidden />
    </form>
  </div>
);
  • UI Elements:
    • <div>: প্রোফাইল ইমেজ প্রদর্শন এবং ফাইল আপলোড বাটন রাখার জন্য কন্টেইনার।
    • <img>: প্রোফাইল ইমেজ দেখানোর জন্য।
    • <form>: ফাইল আপলোড করার জন্য HTML ফর্ম।
    • <button>: আপলোড শুরু করার জন্য বাটন।
    • <input>: type="file" ইনপুট, যা ফাইল আপলোডের জন্য ব্যবহৃত।

ডিফল্ট এক্সপোর্ট

export default ProfileImage;
  • export default: এই কম্পোনেন্টকে অন্য ফাইলে ব্যবহারের জন্য এক্সপোর্ট করছে।

কোডের কার্যপ্রণালী (সংক্ষেপে)

  1. ব্যবহারকারী ইমেজ আপডেট বাটনে ক্লিক করলে handleImageUpload চালু হয়।
  2. এটি fileUploaderRef কে ট্রিগার করে, এবং ব্যবহারকারী ফাইল নির্বাচন করতে পারে।
  3. ফাইল নির্বাচিত হলে updateImageDisplay API কলের মাধ্যমে ফাইল সার্ভারে আপলোড করে।
  4. সফল হলে dispatch এর মাধ্যমে স্টেট আপডেট করে এবং প্রোফাইল ইমেজ পরিবর্তিত হয়।

কিছু অস্পষ্ট থাকলে জানাও! 😊

6. Bio Update

Bio-তে editMode টগল করে বায়ো আপডেট করা যাবে।

import { useState } from "react";
import { useProfile } from "../../context/ProfileContext";
import { actions } from "../../actions";
 
const Bio = () => {
  const { state, dispatch } = useProfile();
  const [editMode, setEditMode] = useState(false);
  const [bio, setBio] = useState(state.user?.bio);
 
  const handleBioSave = async () => {
    dispatch({ type: actions.profile.PROFILE_FETCHING });
 
    try {
      const response = await api.patch(`/profile/${state.user.id}`, { bio });
      dispatch({ type: actions.profile.PROFILE_UPDATED, data: response.data });
      setEditMode(false);
    } catch (error) {
      dispatch({
        type: actions.profile.PROFILE_FETCH_ERROR,
        error: error.message,
      });
    }
  };
 
  return (
    <div>
      {!editMode ? (
        <p>{state.user?.bio}</p>
      ) : (
        <textarea value={bio} onChange={(e) => setBio(e.target.value)} />
      )}
      <button onClick={() => setEditMode(!editMode)}>
        {editMode ? "Save" : "Edit"}
      </button>
    </div>
  );
};
 
export default Bio;

7. Wrap with ProfileProvider

App-এ ProfileProvider যুক্ত করব।

import { ProfileProvider } from "./context/ProfileContext";
 
function App() {
  return (
    <ProfileProvider>
      <Routes>
        <Route path="/profile" element={<ProfilePage />} />
      </Routes>
    </ProfileProvider>
  );
}
 
export default App;

এইভাবে আপনার প্রোফাইল পেজ আরও রিডেবল, স্কেলেবল এবং সহজে মেইনটেইনেবল হয়ে যাবে।