practicepratidin.com
Document Practice Pratidin: Scratch to Finish

সেকশন ১: প্রজেক্ট সেট আপ করা

এখন আমরা ধাপে ধাপে কিভাবে Vite, React.js, TailwindCSS এবং Axios ব্যবহার করে Quiz Application ফ্রন্টএন্ড তৈরি করতে হয় তা আলোচনা করবো।

১. Vite প্রজেক্ট ইনিশিয়ালাইজ করা:

আপনার টার্মিনাল খুলুন এবং নিচের কমান্ডগুলো রান করুন একটি নতুন Vite প্রজেক্ট তৈরি করতে React টেমপ্লেট সহ।

npm create vite@latest my-quiz-app --template react
cd my-quiz-app
npm install

২. TailwindCSS ইনস্টল করা:

TailwindCSS সেট আপ করতে নিচের কমান্ডগুলো রান করুন।

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

tailwind.config.js ফাইলটি কনফিগার করুন যাতে আপনার টেমপ্লেট ফাইলগুলোর পাথ অন্তর্ভুক্ত থাকে।

// tailwind.config.js
module.exports = {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

আপনার CSS ফাইলে Tailwind ডিরেক্টিভ যোগ করুন:

/* ./src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

৩. Axios ও React Router Dom ইনস্টল করা:

API কল করার জন্য Axios ব্যবহার করা হবে।

npm install axios react-router-dom

আপনার Vite, React, এবং TailwindCSS প্রজেক্টে Google Fonts থেকে একাধিক fontFamily ব্যবহার করতে চাইলে নিচের ধাপগুলো অনুসরণ করুন:


সেকশন ২: Customized/Google Fonts সেট আপ

১. পছন্দের Font বেছে নিন

Google Fonts-এ গিয়ে একাধিক ফন্ট বেছে নিন। প্রতিটি ফন্টের জন্য আলাদা লিঙ্ক পাবেন। উদাহরণস্বরূপ:

  • Roboto ফন্টের জন্য:

    <link
      href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap"
      rel="stylesheet"
    />
  • Lobster ফন্টের জন্য:

    <link
      href="https://fonts.googleapis.com/css2?family=Lobster&display=swap"
      rel="stylesheet"
    />

২. ফন্ট লিঙ্কগুলো HTML ফাইলে যোগ করুন

আপনার প্রজেক্টের index.html ফাইলের <head> ট্যাগে এই লিঙ্কগুলো যোগ করুন:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite React Tailwind</title>
 
    <!-- Google Fonts -->
    <link
      href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap"
      rel="stylesheet"
    />
    <link
      href="https://fonts.googleapis.com/css2?family=Lobster&display=swap"
      rel="stylesheet"
    />
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

৩. TailwindCSS কনফিগারেশন আপডেট করুন

tailwind.config.js ফাইলে fontFamily সেটআপ করুন। উদাহরণ:

// tailwind.config.js
module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {
      fontFamily: {
        roboto: ["Roboto", "sans-serif"],
        lobster: ["Lobster", "cursive"],
      },
    },
  },
  plugins: [],
};

৪. React কম্পোনেন্টে ফন্ট ব্যবহার করুন

এখন TailwindCSS এর font-roboto এবং font-lobster ক্লাস ব্যবহার করে ফন্ট অ্যাপ্লাই করতে পারবেন। উদাহরণ:

export default function App() {
  return (
    <div className="p-5">
      <h1 className="text-3xl font-lobster">This is Lobster Font</h1>
      <p className="text-lg font-roboto">This is Roboto Font</p>
    </div>
  );
}

সংক্ষেপে:
Google Fonts এর লিঙ্ক index.html-এ যোগ করুন, তারপর tailwind.config.js-এ fontFamily কনফিগার করুন, এবং অবশেষে TailwindCSS ক্লাস ব্যবহার করে ফন্ট অ্যাপ্লাই করুন।

Ignore .env file .gitignore

If you've accidentally pushed your .env file to your repository and want to ensure it is ignored and removed from the repository, follow these steps:


1. Add .env to .gitignore

  • Open your project directory.
  • Add .env to your .gitignore file (or create a .gitignore file if it doesn't exist).
    Example .gitignore file:
    # Ignore environment files
    .env

2. Remove .env from the Git Repository

Even after adding .env to .gitignore, it won't remove the file from the repository if it's already tracked. You need to untrack it:

  1. Run the following command to untrack the .env file:

    git rm --cached .env
  2. Commit the changes:

    git commit -m "Remove .env file from repository"
  3. Push the changes to your remote repository:

    git push

3. Secure Sensitive Information

If your .env file contains sensitive data (like API keys, database credentials, etc.), consider revoking and regenerating those keys as they might already be compromised.


4. Verify .env is Ignored

To confirm .env is ignored, you can test it by running:

git status

The .env file should no longer appear in the output.


5. Use Environment Variables Securely

For future reference:

  • Always add .env to .gitignore at the start of a project.
  • Avoid pushing sensitive information to repositories. Use tools like GitHub Secrets (opens in a new tab) for secure configuration in CI/CD pipelines.

Let me know if you need help fixing any specific issues!

সেকশন ৩: প্রজেক্ট স্ট্রাকচার সেট আপ করা

প্রয়োজনীয় ফোল্ডার এবং ফাইলগুলো তৈরি করুন আপনার প্রজেক্ট স্ট্রাকচারের জন্য।

১. Project Structure: Practice-Pratidin

Organize the project into components and pages:

src/
|-- components/
|   |-- Navbar.js
|   |-- Footer.js
|-- pages/
|   |-- HomePage.js
|   |-- LoginPage.js
|   |-- RegistrationPage.js
|   |-- QuizPage.js
|   |-- LeaderboardPage.js
|   |-- ResultPage.js
|   |-- admin/
|       |-- AdminDashboard.js
|       |-- AdminQuizSetPage.js
|       |-- AdminQuizSetEntryPage.js
|-- App.js
|-- index.css
|-- main.js
mkdir -p src/components src/pages src/api src/context src/utils
touch src/api/quizAPI.js src/context/AuthContext.js src/utils/PrivateRoute.js

২. কম্পোনেন্ট এবং পেজ তৈরি করা

ধাপে ধাপে HomePage, LoginPage, RegistrationPage, LeaderboardPage, QuizPage, ResultPage, NotFoundPage তাদের পেজ তৈরি করা হবে। এছাড়া Header, Footer কম্পোনেন্ট তৈরি করা হবে। পেজগুলো যত টুকরো টুকরো করা যায়, ততগুলো টুকরো কম্পোনেন্টে ভাগ করা যেতে পারে।

এবং admin ফোল্ডারে আপনার এডমিন প্যানেল AdminDashboardPage, QuizSetPage, QuizSetEntry কম্পোনেন্ট এবং পেজ তৈরি করা হবে।

৩. .env ফাইলে API URL সেট করা

অ্যাপ্লিকেশনের বেস ইউআরএল .env ফাইলে সেট করা হয়।

VITE_SERVER_BASE_URL=http://localhost:3000/api

এই URL ব্যবহার করে অ্যাপ্লিকেশন থেকে API কল করা হয়।


সেকশন ৪: রাউটিং যোগ করা, প্রাইভেট রাউট ইমপ্লিমেন্ট করা

১. আমাদের এপ্রোচ তৈরি

এখানে আমরা React Router Dom ব্যবহার করে রাউটিং সেটআপ করবো। প্রাইভেট রাউট তৈরি করে ব্যবহারকারী লগইন করা থাকলে প্রাইভেট পেজে প্রবেশ করতে পারবে।

এই প্রজেক্ট করার পর, আমাদের Aprroch যেন এমন হয়ে যায়। যেন যেকোনো প্রজেক্ট করতে গেলে, ঠিক এই একই এপ্রোচে আগাতে পারি।

এখন আমাদের লগইন ইউজারের ইনফরমেশন স্টোর করতে হবে। এই প্রজেক্টে আমরা ইন মেমরি স্টোর করব। আগের ডকুমেন্টে (Rongtul Project) ইন মেমরি কিভাবে ডাটা স্টোর করা যায়। বিস্তারিত বলেছি।

১। এখন Context API দিয়ে, আমরা AuthContext তৈরি করব।

২। এবং AuthProvider তৈরি করে, পুরো এপ্লিকেশনকে মুড়িয়ে দিবো। সেখানে আমরা লগইন করা ইউজারের তথ্য স্টোর করবো।

৩। এবং useAuth নামে একটি কাস্টম হুক বানাবো, যার মাধ্যমে আমরা login user এর তথ্য নিয়ে সকল পেজে খেলতে পারবো। যাতে করে বার বার useContext এবং AuthContext সব পেজে import করা না লাগে।

যাতে accross the page অর্থাৎ ওয়েবসাইটের সব পেজ এ আমরা এই ডাটা শেয়ার করতে পারি।

২. AuthContext, AuthProvider, and useAuth Hook তৈরি করা

context/AuthContext.jsx

import { createContext } from "react";
const AuthContext = createContext();
 
export default AuthContext;

providers/AuthProvider.jsx

import { useState } from "react";
import AuthContext from "../context/AuthContext";
 
export default function AuthProvider({ children }) {
  const [auth, setAuth] = useState("");
 
  return (
    <AuthContext.Provider value={{ auth, setAuth }}>
      {children}
    </AuthContext.Provider>
  );
}

hooks/useAuth.js

import { useContext } from "react";
import AuthContext from "../context/AuthContext";
 
export default function useAuth() {
  return useContext(AuthContext);
}

এখন আমরা Private Route তৈরি করব। যার মাধ্যমে কিছু কিছু রাউট প্রাইভেট করব। যদি auth এর মধ্যে user থাকে, তাহলে প্রাইভেট রাউটে ঢুকতে পারবে। তা না হলে ধাক্কা মেরে লগইন পেজে নিয়ে যাবো।

৩. Private Route বানানো

routes/PrivateRoutes.jsx

import { Navigate, Outlet } from "react-router-dom";
import Header from "../components/common/Header";
import useAuth from "../hooks/useAuth";
export default function PrivateRoutes() {
  const { auth } = useAuth();
  return (
    <>
      {auth.user ? (
        <>
          <Header />
          <main className="mx-auto max-w-[1020px] py-8">
            <div className="container">
              <Outlet />
            </div>
          </main>
        </>
      ) : (
        <Navigate to="/login" />
      )}
    </>
  );
}
  • auth.user: যদি ব্যবহারকারী লগইন করা থাকে, তাহলে চাইল্ড রাউটগুলো (যা Outlet এর মাধ্যমে রেন্ডার হয়) দেখাবে।
  • <Navigate to="/login" />: যদি ব্যবহারকারী লগইন না থাকে, তাহলে তাকে /login পেজে পাঠানো হবে।

Outlet এর কাজ কী?

<Outlet /> React Router-এ nested routes রেন্ডার করার জন্য ব্যবহৃত হয়। এটি মূলত চাইল্ড রাউটগুলোর জন্য একটি জায়গা তৈরি করে, যেখানে রাউট অনুযায়ী ডাইনামিক কন্টেন্ট দেখানো যায়।

উদাহরণ:

<Route path="/dashboard" element={<PrivateRoute />}>
  <Route path="profile" element={<ProfilePage />} />
  <Route path="settings" element={<SettingsPage />} />
</Route>
  • এখানে PrivateRoute এর মধ্যে <Outlet /> রেন্ডার হবে, এবং path অনুযায়ী কখনো <ProfilePage />, কখনো <SettingsPage /> দেখাবে।

<Outlet /> ট্যাগকে div এবং main দিয়ে মুড়িয়ে দিলাম কেন?

  1. স্টাইল এবং লেআউটের জন্য:

    • <main>: HTML এর একটি সেম্যান্টিক ট্যাগ। এটি মূল কনটেন্টের জন্য ব্যবহৃত হয়। সার্চ ইঞ্জিন এবং স্ক্রিন রিডার বুঝতে পারে এটি মূল কন্টেন্ট।
    • <div>: এটি নির্দিষ্ট সেকশনের স্টাইল এবং লেআউট ম্যানেজ করার জন্য।
  2. রেসপন্সিভ লেআউট:

    • className="mx-auto max-w-[1020px] py-8": এখানে রেসপন্সিভ লেআউট তৈরি করার জন্য main এবং div ব্যবহার করা হয়েছে। এটি কন্টেন্টকে কেন্দ্রস্থলে রাখে এবং একটি নির্দিষ্ট প্রস্থ দেয়।
  3. কোডের রিডেবিলিটি:

    • এটি কোডকে আরও গঠনমূলক ও রিডেবল করে। ভবিষ্যতে নতুন ক্লাস বা লেআউট অ্যাড করাও সহজ হয়।

৪. স্ট্রাকচার Protected Routes and Public Routes in App.js

import { Route, BrowserRouter as Router, Routes } from "react-router-dom";
import HomePage from "./components/HomePage";
import LoginPage from "./components/LoginPage";
import AuthProvider from "./providers/AuthProvider";
import PrivateRoutes from "./routes/PrivateRoutes";
export default function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          {/* Private Routes */}
          <Route element={<PrivateRoutes />}>
            <Route path="/" element={<HomePage />} />
          </Route>
 
          {/* Public Routes */}
          <Route path="/login" element={<LoginPage />} />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

সেকশন ৫: জেসন ওয়েব টোকেন ইমপ্লিমেন্টেশন (JWT)

এই প্রজেক্টে JWT ব্যবহার করে Quiz Application-এ ইউজারের অথেন্টিকেশন এবং অথোরাইজেশন ইমপ্লিমেন্ট করা হয়েছে। এই ডকুমেন্টে ধাপে ধাপে পুরো প্রসেসটি তুলে ধরা হয়েছে।


স্টেপ ১: লগইন এবং টোকেন রিসিভ করা

লগইন ফর্ম সাবমিট করার পর নিচের ধাপগুলো ঘটে:

  1. API কল:

    • ইউজারের দেওয়া ডেটা (ইমেইল এবং পাসওয়ার্ড) ব্যাকএন্ডে পাঠানো হয়।
    • এই জন্য Axios POST ব্যবহার করা হয়।
  2. রেসপন্স:

    • সঠিক তথ্য দিলে ব্যাকএন্ড থেকে নিচের ফরম্যাটে ডেটা রিটার্ন হয়:
{
  "status": "success",
  "data": {
    "user": {
      "id": "19a255dd-2b72-48f1-9bef-a09e60cb0903",
      "full_name": "Md. Mojnu Miah",
      "email": "thisismojnu@gmail.com",
      "role": "user",
      "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjE5YTI1NWRkLTJiNzItNDhmMS05YmVmLWEwOWU2MGNiMDkwMyIsImlhdCI6MTczNTU2MzQ1NCwiZXhwIjoxNzM1NTYzNzU0fQ.el6ZNkCk_MrmKYZy4YpeBjvL3KhrjcZkshTHSmsGPYM",
      "createdAt": "2024-12-30T12:56:53.946Z",
      "updatedAt": "2024-12-30T12:57:34.212Z"
    },
    "tokens": {
      "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjE5YTI1NWRkLTJiNzItNDhmMS05YmVmLWEwOWU2MGNiMDkwMyIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzM1NTYzNDU0LCJleHAiOjE3MzU1NjM1MTR9.90CobrtMrHCD0CPkZNE1jBK6eYQpXPJoaO3njslR8MU",
      "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjE5YTI1NWRkLTJiNzItNDhmMS05YmVmLWEwOWU2MGNiMDkwMyIsImlhdCI6MTczNTU2MzQ1NCwiZXhwIjoxNzM1NTYzNzU0fQ.el6ZNkCk_MrmKYZy4YpeBjvL3KhrjcZkshTHSmsGPYM"
    }
  }
}
  1. ডেটা সংরক্ষণ:
    • রেসপন্স থেকে user, accessToken, এবং refreshToken আলাদা করে স্টেটে সংরক্ষণ করা হয়।
    • Error Handling: ভুল তথ্য দিলে try-catch ব্লকের মাধ্যমে এরর হ্যান্ডল করে UI-তে দেখানো হয়।

কোড উদাহরণ:

const onSubmit = async (formData) => {
  try {
    const response = await axios.post(
      `${import.meta.env.VITE_SERVER_BASE_URL}/auth/login`,
      formData
    );
 
    console.log("Login response:", response?.data);
 
    if (response.status === 200) {
      const { user, tokens } = response?.data?.data;
      const { accessToken, refreshToken } = tokens;
 
      // সঠিকভাবে `authToken` সেট করা হচ্ছে
      setAuth({ user, authToken: accessToken, refreshToken });
      navigate("/"); // হোমপেজে রিডাইরেক্ট
    }
  } catch (error) {
    console.error("Login error:", error.message);
  }
};

স্টেপ ২: .env ফাইলে API URL সেট করা

অ্যাপ্লিকেশনের বেস ইউআরএল .env ফাইলে সেট করা হয়।

VITE_SERVER_BASE_URL=http://localhost:5000/api

এই URL ব্যবহার করে অ্যাপ্লিকেশন থেকে API কল করা হয়।


স্টেপ ৩: টোকেন এক্সপায়ার হলে রিফ্রেশ টোকেন ব্যবহার করা

JWT-এর মেয়াদ থাকে নির্দিষ্ট সময়ের জন্য (যেমন ৩০ মিনিট)। মেয়াদ শেষ হলে 401 Unauthorized এরর পাওয়া যায়। এ ক্ষেত্রে Refresh Token ব্যবহার করে নতুন টোকেন জেনারেট করা হয়।

স্টেপ ৪: Axios Instance এবং Interceptor তৈরি করা

অ্যাপ্লিকেশনের প্রতিটি API রিকোয়েস্টে টোকেন ব্যবহারের জন্য Axios Instance তৈরি করা হয়েছে।

  1. Request Interceptor:

    • প্রতিটি রিকোয়েস্টের হেডারে Authorization Token যোগ করা হয়।
  2. Response Interceptor:

    • টোকেন এক্সপায়ার হয়ে গেলে Refresh Token ব্যবহার করে নতুন টোকেন জেনারেট করা হয় এবং আগের রিকোয়েস্টটি পুনরায় করা হয়।

কোড উদাহরণ:


স্টেপ ৫: Custom Hook (useAxios) তৈরি করা

useAxios হুক ব্যবহার করে সেন্ট্রালাইজড ইন্টারসেপ্টর ইমপ্লিমেন্ট করা হয়েছে।

কোড:

import axios from "axios";
import { useEffect } from "react";
import { useAuth } from './useAuth';
 
 
 
export default function useAxios() {
    const { auth, setAuth } = useAuth();
 
 
 
    const api = axios.create({
        baseURL: import.meta.env.VITE_SERVER_BASE_URL,
    });
 
 
    useEffect(() => {
        const requestIntercept = api.interceptors.request.use(
            (config) => {
                const authToken = auth?.authToken;
                if (authToken) {
                    config.headers.Authorization = `Bearer ${authToken}`;
                }
                return config;
            },
            (error) => Promise.reject(error)
        );
 
        const responseIntercept = api.interceptors.response.use(
            (response) => response,
            async (error) => {
                const originalRequest = error.config;
 
                if (error.response?.status === 401 && !originalRequest._retry) {
                    originalRequest._retry = true;
 
                    try {
                        const response = await api.post("/auth/refresh-token", {
                            refreshToken: auth?.refreshToken,
                        });
 
                        const { token } = response.data;
                        setAuth({ ...auth, authToken: token });
 
                        originalRequest.headers.Authorization = `Bearer ${token}`;
                        return api(originalRequest);
                    } catch (err) {
                        return Promise.reject(err);
                    }
                }
                return Promise.reject(error);
            }
        );
 
        return () => {
            api.interceptors.request.eject(requestIntercept);
            api.interceptors.response.eject(responseIntercept);
        };
    }, [auth?.authToken]);
 
    return { api };
};
 
export default useAxios;

স্টেপ ৬: সারাংশ

  1. লগইন: ইউজার লগইন করলে Access Token এবং Refresh Token জেনারেট হয়।
  2. Token Handling:
    • প্রতিটি API রিকোয়েস্টে টোকেন ব্যবহার করা হয়।
    • টোকেন এক্সপায়ার হলে Refresh Token দিয়ে নতুন টোকেন আনা হয়।
  3. Custom Hook: useAxios হুক ব্যবহার করে সেন্ট্রালাইজড ইন্টারসেপ্টর ইমপ্লিমেন্ট করা হয়েছে।
  4. ডেটা ফেচ: টোকেনসহ প্রোফাইল ডেটা ফেচ করা হয়েছে।

এই স্টেপগুলো অনুসরণ করে Quiz Application-এ সুরক্ষিত অথেন্টিকেশন ও অথোরাইজেশন ইমপ্লিমেন্ট করা হয়েছে।

সেকশন ৬: Functional Login Form Validation and with React Hook Form and Yup

১. ডিপেনডেন্সি ইন্সটল করুন

প্রথমে প্রয়োজনীয় প্যাকেজগুলো ইন্সটল করুন:

npm install react-hook-form @hookform/resolvers yup

২. ভ্যালিডেশন স্কিমা তৈরি করুন

ইউজার ইনপুট ভ্যালিডেশনের জন্য Yup ব্যবহার করা হবে।

ফাইল: validationSchemas/loginSchema.js

import * as Yup from "yup";
 
export const loginValidationSchema = Yup.object().shape({
  email: Yup.string()
    .email("সঠিক ইমেইল ফরম্যাট দিন")
    .required("ইমেইল দেওয়া আবশ্যক"),
  password: Yup.string()
    .min(8, "পাসওয়ার্ড অন্তত ৮ অক্ষরের হতে হবে")
    .required("পাসওয়ার্ড আবশ্যক"),
});

৩. পুনর্ব্যবহারযোগ্য ইনপুট কম্পোনেন্ট তৈরি করুন

ইনপুট ফিল্ডগুলোর জন্য একটি রিইউজেবল TextInput কম্পোনেন্ট তৈরি করা হলো।

ফাইল: components/TextInput.jsx

import React from "react";
 
const TextInput = ({ label, type, id, register, error, placeholder }) => {
  return (
    <div className="mb-4">
      <label htmlFor={id} className="block mb-2">
        {label}
      </label>
      <input
        id={id}
        type={type}
        className={`w-full px-4 py-3 rounded-lg border ${
          error ? "border-red-500" : "border-gray-300"
        }`}
        placeholder={placeholder}
        {...register(id)}
      />
      {error && <p className="text-red-500 text-sm mt-1">{error.message}</p>}
    </div>
  );
};
 
export default TextInput;

৪. লগইন ফর্ম তৈরি করুন

react-hook-form ব্যবহার করে LoginForm কম্পোনেন্টটি তৈরি করা হলো।

ফাইল: components/LoginForm.jsx

import React from "react";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { loginValidationSchema } from "../validationSchemas/loginSchema";
import TextInput from "./TextInput";
 
const LoginForm = ({ onLogin }) => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: yupResolver(loginValidationSchema),
  });
 
  const onSubmit = (data) => {
    onLogin(data);
  };
 
  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <TextInput
        label="ইউজারনেম অথবা ইমেইল অ্যাড্রেস দিন"
        type="email"
        id="email"
        placeholder="ইউজারনেম অথবা ইমেইল"
        register={register}
        error={errors.email}
      />
      <TextInput
        label="আপনার পাসওয়ার্ড দিন"
        type="password"
        id="password"
        placeholder="পাসওয়ার্ড"
        register={register}
        error={errors.password}
      />
      <div className="mb-6 flex gap-2 items-center">
        <input
          type="checkbox"
          id="admin"
          {...register("admin")}
          className="rounded-lg border border-gray-300"
        />
        <label htmlFor="admin" className="block">
          অ্যাডমিন হিসেবে লগইন
        </label>
      </div>
      <button
        type="submit"
        className="w-full bg-primary text-white py-3 rounded-lg mb-4"
      >
        সাইন ইন
      </button>
    </form>
  );
};
 
export default LoginForm;

৫. LoginPage কম্পোনেন্ট তৈরি করুন

এখন LoginForm ব্যবহার করে LoginPage তৈরি করা হলো।

ফাইল: pages/LoginPage.jsx

import React from "react";
import LoginForm from "../components/LoginForm";
 
const LoginPage = () => {
  const handleLogin = (data) => {
    console.log("লগইন ডেটা সাবমিট:", data);
 
    // এখানে API কল করে লগইন প্রক্রিয়া সম্পন্ন করুন।
    // উদাহরণ:
    // axios.post("/auth/login", data)
    //   .then(response => console.log(response))
    //   .catch(error => console.error(error));
  };
 
  return (
    <div className="w-full lg:w-1/2 flex items-center justify-center p-12">
      <div className="w-full max-w-md">
        <h2 className="text-3xl font-bold mb-8 flex gap-2 items-center">
          <span>স্বাগতম</span>
          <img src="./assets/logo.svg" className="h-7" alt="লোগো" />
        </h2>
        <h1 className="text-5xl font-bold mb-8">সাইন ইন করুন</h1>
        <LoginForm onLogin={handleLogin} />
        <div className="text-center">
          <a href="#" className="text-primary">
            পাসওয়ার্ড ভুলে গেছেন?
          </a>
        </div>
        <div className="mt-8">
          <p className="text-center">
            একাউন্ট নেই?{" "}
            <a href="#" className="text-primary">
              সাইন আপ করুন
            </a>
          </p>
        </div>
      </div>
    </div>
  );
};
 
export default LoginPage;

৬. Tailwind কনফিগারেশন আপডেট করুন

প্রাথমিক কালার primary কনফিগার করুন।

ফাইল: tailwind.config.js

module.exports = {
  theme: {
    extend: {
      colors: {
        primary: "#3498db", // এখানে আপনার পছন্দের কালার সেট করুন
      },
    },
  },
  plugins: [],
};

৭. অ্যাপ রান এবং টেস্ট করুন

অ্যাপ চালু করুন:

npm run dev

টেস্ট করুন:

  • ফর্ম সাবমিট না করে চেষ্টা করুন।
  • ভুল ইমেইল ফরম্যাট দিন।
  • ৮ অক্ষরের কম পাসওয়ার্ড দিন।

বৈশিষ্ট্যসমূহ

  1. ভালিডেশন: ইনপুটগুলো সঠিক কিনা চেক করা।
  2. এরর ডিসপ্লে: প্রতিটি ইনপুটের পাশে এরর মেসেজ দেখানো।
  3. পুনর্ব্যবহারযোগ্য কোড: TextInput কম্পোনেন্ট বারবার ব্যবহার করা যায়।
  4. প্রফেশনাল স্ট্রাকচার: স্কেলেবল এবং মেইনটেন করা সহজ।

ধাপ ৯: কুইজ ফাংশনালিটি ইমপ্লিমেন্ট করা

  1. QuizPage কম্পোনেন্ট তৈরি করুন:
// src/pages/QuizPage.jsx
import React, { useState, useEffect } from "react";
import { fetchQuizById, submitQuiz } from "../api/quizAPI";
 
const QuizPage = () => {
  const [questions, setQuestions] = useState([]);
  const [currentQuestion, setCurrentQuestion] = useState(0);
  const [answers, setAnswers] = useState({});
 
  useEffect(() => {
    const fetchQuiz = async () => {
      const response = await fetchQuizById(1); // ধরুন কুইজ আইডি 1
      setQuestions(response.data.questions);
    };
 
    fetchQuiz();
  }, []);
 
  const handleAnswer = (questionId, answer) => {
    setAnswers((prev) => ({ ...prev, [questionId]: answer }));
  };
 
  const handleSubmit = async () => {
    const response = await submitQuiz(answers);
    // রেসপন্স হ্যান্ডল করুন (যেমন, রেজাল্ট পেজে ন্যাভিগেট করুন)
  };
 
  if (questions.length === 0) {
    return <div>লোড হচ্ছে...</div>;
  }
 
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold">Quiz Page</h1>
      <div>
        <p>{questions[currentQuestion].question}</p>
        <div>
          {questions[currentQuestion].options.map((option) => (
            <button
              key={option}
              onClick={() =>
                handleAnswer(questions[currentQuestion].id, option)
              }
            >
              {option}
            </button>
          ))}
        </div>
        <button onClick={() => setCurrentQuestion((prev) => prev + 1)}>
          Next
        </button>
      </div>
      <button onClick={handleSubmit}>Submit</button>
    </div>
  );
};
 
export default QuizPage;

ধাপ ১০: স্টাইলিং এবং চূড়ান্ত স্পর্শ

  1. প্রয়োজন অনুযায়ী আপনার কম্পোনেন্টগুলোতে TailwindCSS স্টাইল প্রয়োগ করুন যাতে আপনার ডিজাইন মিলে যায়।

  2. আপনার অ্যাপ্লিকেশনটি ভালোভাবে পরীক্ষা করুন যাতে সব ফিচার ঠিকমতো কাজ করছে।

উপসংহার

এই গাইডটি Vite ব্যবহার করে একটি React প্রজেক্ট সেট আপ করা, TailwindCSS দিয়ে স্টাইল করা এবং Axios দিয়ে API কল করা সহ কুইজ অ্যাপ্লিকেশনের কিছু মূল ফিচার ইমপ্লিমেন্ট করার বেসিক বিষয়গুলো কভার করে। আপনি প্রোভাইড করা সোর্স কোড এবং লাইভ ডেমোগুলোর বিস্তারিত ডিজাইন এবং ফাংশনালিটি দেখতে পারেন। আরও উন্নত ফিচারগুলোর জন্য, আপনাকে কম্পোনেন্টগুলো উন্নত করতে, স্টেট ম্যানেজমেন্ট (যেমন, Redux) যোগ করতে এবং প্রয়োজন অনুযায়ী এজ কেসগুলোর হ্যান্ডল করতে হতে পারে।

Feature Implement Start (Step by Step)

সেকশন ৭: হোম পেজ ইমপ্লিমেন্টেশন (Home Page Implementation)

এখন পর্যন্ত আমরা এই কোডের বিভিন্ন অংশকে মডিউলার বা কম্পোনেন্ট-ভিত্তিক ভাবে ভাগ করে একটি HomePage তৈরি করেছি। প্রতিটি কম্পোনেন্ট এবং তাদের ভূমিকা নিচে উল্লেখ করা হলো:


১. Header Component

Header কম্পোনেন্টটি ন্যাভিগেশন ও লগইন/লগআউট বাটনের জন্য ব্যবহার করা হয়েছে। এটি একটি কমন কম্পোনেন্ট, তাই এটি common/Header.jsx ফাইলে রাখা হয়েছে।

// common/Header.jsx
import React from "react";
 
import { useNavigate } from "react-router-dom";
import { useAuth } from "./../../hooks/useAuth";
export default function Header() {
  const { auth, setAuth } = useAuth();
  const navigate = useNavigate();
 
  function handleLogout() {
    setAuth(null);
  }
 
  return (
    <header className="flex justify-between items-center mb-12">
      <img src="./assets/logo.svg" className="h-7" />
      <div>
        {auth?.user ? (
          <button
            className="px-4 py-2 rounded hover:bg-primary hover:text-white transition-colors font-jaro"
            onClick={handleLogout}
          >
            Logout
          </button>
        ) : (
          <button
            className="px-4 py-2 rounded hover:bg-primary hover:text-white transition-colors font-jaro"
            onClick={() => navigate("/login")}
          >
            Login
          </button>
        )}
      </div>
    </header>
  );
}

২. Footer Component

Footer কম্পোনেন্টটি একাধিক পেজে ব্যবহার করা যেতে পারে, তাই এটি common/Footer.jsx ফাইলে রাখা হয়েছে।

// common/Footer.jsx
import React from "react";
 
const Footer = () => {
  return (
    <footer className="mt-6 mb-3 opacity-40 text-center">
      Copyright &copy; 2024 Learn With Sumit | All Rights Reserved
    </footer>
  );
};
 
export default Footer;

৩. UserInfo Component

ব্যবহারকারীর প্রোফাইল তথ্য দেখানোর জন্য UserInfo কম্পোনেন্ট ব্যবহার করা হয়েছে।

// components/UserInfo.jsx
import { useAuth } from "./../hooks/useAuth";
export default function UserInfo() {
  const { auth } = useAuth();
  return (
    <>
      {auth?.user && (
        <div className="text-center mb-12">
          <img
            src="./assets/avater.webp"
            alt="Profile Picture"
            className="w-32 h-32 rounded-full border-4 border-primary mx-auto mb-4 object-cover"
          />
          <p className="text-xl text-gray-600">Welcome</p>
          <h2 className="text-4xl font-bold text-gray-700 font-jaro">
            {auth?.user?.full_name}
          </h2>
        </div>
      )}
    </>
  );
}

৪. QuizCard Component

Quiz-এর ডেটা ডাইনামিকভাবে দেখানোর জন্য QuizCard কম্পোনেন্ট তৈরি করা হয়েছে।

// components/QuizCard.jsx
import { Link } from "react-router-dom";
export default function QuizCard({
  quizId,
  title,
  description,
  thumbnail,
  totalQuestion,
  isAttempted,
}) {
  return (
    <Link
      to={isAttempted ? `/quiz/${quizId}/leaderboard` : `/quiz/${quizId}`}
      className="rounded-lg overflow-hidden shadow-lg hover:shadow-xl transition-shadow max-h-[450px] relative group cursor-pointer"
    >
      <div className="group-hover:scale-105 absolute transition-all text-white  text-center top-1/2 -translate-y-1/2 px-4">
        <h1 className=" text-5xl font-jaro">
          {title} {totalQuestion}
        </h1>
        <p className="mt-2 text-lg">{description}</p>
      </div>
      {isAttempted && (
        <div className="hidden absolute transition-all bg-black/80 w-full h-full left-0 top-0 text-white group-hover:grid place-items-center">
          <div>
            <h1 className="text-3xl font-bold">Already Participated</h1>
            <p className="text-center">Click to view your leaderboard</p>
          </div>
        </div>
      )}
 
      <img
        src={thumbnail}
        alt={title}
        className="w-full h-full object-cover rounded mb-4"
      />
    </Link>
  );
}

৫. HomePage Component

উপরের সমস্ত কম্পোনেন্টগুলো HomePage-এ একত্র করা হয়েছে।

// pages/HomePage.jsx
import { useEffect } from "react";
import Footer from "../components/common/Footer";
import Header from "../components/common/Header";
import QuizCard from "../components/QuizCard";
import UserInfo from "../components/UserInfo";
import useAxios from "../hooks/useAxios";
 
export default function HomePage() {
  const { api } = useAxios();
  const quizSet = {
    status: "success",
    data: [
      {
        id: "139561c6-7108-4635-82c2-f933f58f362c",
        title: "Chemistry- Chemical Bond",
        description:
          "যদি এই ২১ টা MCQ এর উত্তর সঠিক দিতে পারো, তাহলে বুঝবে তুমি বস!",
        thumbnail: "http://localhost:5000/images/1.jpeg",
        status: "published",
        created_at: "2024-12-31T02:44:58.707Z",
        total_questions: 2,
        total_attempts: 0,
        is_attempted: false,
      },
      {
        id: "3ac55a23-848d-486e-b820-d2c9a24bc62c",
        title: "Chemistry- Structure of Matter",
        description:
          "যদি এই ৩০ টা MCQ এর উত্তর সঠিক দিতে পারো, তাহলে বুঝবে তুমি ফাটায়া দিছো!",
        thumbnail: "http://localhost:5000/images/7.jpg",
        status: "published",
        created_at: "2024-12-31T02:13:52.258Z",
        total_questions: 2,
        total_attempts: 0,
        is_attempted: false,
      },
    ],
  };
 
  useEffect(() => {
    // Fetch quiz set data from server
    async function fetchQuestionSets() {
      try {
        const response = await api.get(
          `${import.meta.env.VITE_SERVER_BASE_URL}/quizzes`
        );
 
        console.log("Quiz set response:", response?.data);
      } catch (error) {
        console.error("Quiz set error:", error.message);
      }
    }
    fetchQuestionSets();
  });
 
  return (
    <div className="container mx-auto py-3">
      <Header />
      <UserInfo />
      <main className="bg-white p-6 rounded-md h-full">
        <section>
          <h3 className="text-2xl font-bold mb-6">Participate In Quizees</h3>
          <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-6">
            {quizSet.data.map((quiz) => (
              <QuizCard
                key={quiz.id}
                quizId={quiz?.id}
                title={quiz?.title}
                description={quiz?.description}
                thumbnail={quiz?.thumbnail}
                totalQuestion={quiz?.total_questions}
                isAttempted={quiz?.is_attempted}
              />
            ))}
          </div>
        </section>
      </main>
      <Footer />
    </div>
  );
}

আমরা এখন পর্যন্ত যা করেছি

  1. Header Component: নেভিগেশন বার এবং লগইন/লগআউট বাটন তৈরি।
  2. Footer Component: সাধারণ ফুটার তৈরি।
  3. UserInfo Component: ব্যবহারকারীর প্রোফাইল তথ্য দেখানোর জন্য।
  4. QuizCard Component: Quiz গুলো ডাইনামিকভাবে দেখানোর জন্য।
  5. HomePage Component: উপরের সব কম্পোনেন্ট একত্র করে একটি পূর্ণাঙ্গ হোমপেজ তৈরি।

এখন আপনি সহজেই এই প্রজেক্টে নতুন ফিচার যোগ করতে পারবেন। 😊

সেকশন ৮: কুইজ পেজ ইমপ্লিমেন্টেশন (Quiz Page Implementation)

এই ধাপে, আমরা কুইজ পেজের ডিজাইন এবং ফাংশনালিটি ইমপ্লিমেন্ট করব। এই পেজে কুইজ প্রশ্ন এবং উত্তর দেখানো হবে। প্রতিটি প্রশ্নের জন্য একটি বাটন থাকবে যা ক্লিক করলে পরবর্তী প্রশ্নে যাওয়া যাবে।

অবশ্যই! এখানে আমি QuizPage কম্পোনেন্টটিকে ছোট ছোট কম্পোনেন্টে ভাগ করে দিয়েছি এবং প্রতিটি অংশের ব্যাখ্যা বাংলায় করেছি:

Step 1: QuizHeader কম্পোনেন্ট তৈরী

function QuizHeader() {
  return (
    <div>
      <h2 className="text-4xl font-bold mb-4">React Hooks Quiz</h2>
      <p className="text-gray-600 mb-4">
        A quiz on React hooks like useState, useEffect, and useContext.
      </p>
    </div>
  );
}

ব্যাখ্যা: QuizHeader কম্পোনেন্টটি কুইজের শিরোনাম এবং বিবরণ প্রদর্শন করবে।

Step 2: InfoBadge কম্পোনেন্ট তৈরী

function InfoBadge({ bgColor, textColor, text }) {
  return (
    <div
      className={`w-fit ${bgColor} ${textColor} text-sm font-medium px-2.5 py-0.5 rounded-full inline-block mb-2`}
    >
      {text}
    </div>
  );
}

ব্যাখ্যা: InfoBadge কম্পোনেন্টটি একটি ব্যাজ প্রদর্শন করবে। এটি পুনঃব্যবহারযোগ্য এবং বিভিন্ন রঙ ও টেক্সট সহ ব্যবহার করা যাবে।

Step 3: QuizInfo কম্পোনেন্ট তৈরী

function QuizInfo() {
  return (
    <div className="flex flex-col">
      <InfoBadge
        bgColor="bg-green-100"
        textColor="text-green-800"
        text="Total number of questions : 10"
      />
      <InfoBadge
        bgColor="bg-blue-100"
        textColor="text-blue-800"
        text="Participation : 1"
      />
      <InfoBadge
        bgColor="bg-gray-100"
        textColor="text-green-800"
        text="Remaining : 9"
      />
    </div>
  );
}

ব্যাখ্যা: QuizInfo কম্পোনেন্টটি বিভিন্ন তথ্য ব্যাজ প্রদর্শন করবে।

Step 4: UserAvatar কম্পোনেন্ট তৈরী

function UserAvatar() {
  return (
    <div className="mt-auto flex items-center">
      <img
        src="./assets/avater.webp"
        alt="Mr Hasan"
        className="w-10 h-10 rounded-full mr-3 object-cover"
      />
      <span className="text-black font-semibold">Saad Hasan</span>
    </div>
  );
}

ব্যাখ্যা: UserAvatar কম্পোনেন্টটি ব্যবহারকারীর ছবি এবং নাম প্রদর্শন করবে।

Step 5: Option কম্পোনেন্ট তৈরী

function Option({ name, option }) {
  return (
    <label className="flex items-center space-x-3 py-3 px-4 bg-primary/5 rounded-md text-lg">
      <input
        type="checkbox"
        name={name}
        className="form-radio text-buzzr-purple"
      />
      <span>{option}</span>
    </label>
  );
}

ব্যাখ্যা: Option কম্পোনেন্টটি প্রতিটি প্রশ্নের অপশন প্রদর্শন করবে।

Step 6: Options কম্পোনেন্ট তৈরী

function Options() {
  const options = ["0", "-1", "1", "1"];
  return (
    <div className="grid grid-cols-2 gap-4">
      {options.map((option, index) => (
        <Option key={index} name={`answer${index + 1}`} option={option} />
      ))}
    </div>
  );
}

ব্যাখ্যা: Options কম্পোনেন্টটি সকল অপশন গুলোকে প্রদর্শন করবে।

Step 7: Question কম্পোনেন্ট তৈরী

function Question() {
  return (
    <div className="bg-white p-6 !pb-2 rounded-md">
      <div className="flex justify-between items-center mb-4">
        <h3 className="text-2xl font-semibold">
          3. What is the height of an empty binary tree?
        </h3>
      </div>
      <Options />
      <NextButton />
    </div>
  );
}

ব্যাখ্যা: Question কম্পোনেন্টটি প্রশ্ন এবং অপশন গুলো প্রদর্শন করবে।

Step 8: NextButton কম্পোনেন্ট তৈরী

function NextButton() {
  return (
    <a
      href="./result.html"
      className="w-1/2 text-center ml-auto block bg-primary text-white py-2 px-4 rounded-md hover:bg-indigo-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary mb-6 font-semibold my-8"
    >
      Next
    </a>
  );
}

ব্যাখ্যা: NextButton কম্পোনেন্টটি "Next" বোতাম প্রদর্শন করবে।

Step 9: QuizPage মূল কম্পোনেন্ট

export default function QuizPage() {
  return (
    <main className="max-w-8xl mx-auto h-[calc(100vh-10rem)]">
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-10 h-full">
        <div className="lg:col-span-1 bg-white rounded-md p-6 h-full flex flex-col">
          <QuizHeader />
          <QuizInfo />
          <UserAvatar />
        </div>
        <div className="lg:col-span-2 bg-white">
          <Question />
        </div>
      </div>
    </main>
  );
}

ব্যাখ্যা: QuizPage মূল কম্পোনেন্টটি সমস্ত ছোট কম্পোনেন্ট গুলোকে একত্রে প্রদর্শন করবে।

এইভাবে, কম্পোনেন্ট গুলোকে ছোট ছোট অংশে ভাগ করে কোডটি আরও মডুলার এবং রক্ষণাবেক্ষণযোগ্য করা হয়েছে।

স্টেপ ১০: How to send data to QuizPage from QuizCard? (React Router)

There is No Relation QuizPage and QuizCard. Explain Briefly the technique in Bangla. We need send quizId to QuizPage from QuizCard.

QuizCard থেকে QuizPage-এ quizId পাঠানোর জন্য আমরা React Router ব্যবহার করতে পারি। এই ক্ষেত্রে Link কম্পোনেন্টটি একটি নির্দিষ্ট quizId সহ একটি URL-এ রিডাইরেক্ট করে। QuizPage সেই URL থেকে quizId পেতে পারে। নিচে এই পুরো প্রক্রিয়া বাংলা ভাষায় ব্যাখ্যা করা হলো:


১. Link এর মাধ্যমে quizId পাঠানো:

QuizCard-এ Link কম্পোনেন্ট ব্যবহার করে আমরা to প্রপার্টিতে একটি ডাইনামিক URL সেট করি। উদাহরণস্বরূপ:

<Link to={`/quiz/${quizId}`}>

এখানে /quiz/${quizId} একটি ডাইনামিক রুট যা quizId এর মান URL-এ যোগ করে।


২. Route সেটআপ করা:

React Router-এ QuizPage এর জন্য রুট কনফিগার করতে হবে। ডাইনামিক রুট প্যারামিটার ব্যবহার করে এটি করা হয়। উদাহরণ:

import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import QuizPage from "./QuizPage";
import QuizCard from "./QuizCard";
 
function App() {
  return (
    <Router>
      <Routes>
        <Route path="/quiz/:quizId" element={<QuizPage />} />
        <Route path="/" element={<QuizCard />} />
      </Routes>
    </Router>
  );
}
 
export default App;

এখানে /quiz/:quizId হচ্ছে ডাইনামিক রুট যেখানে :quizId হলো একটি প্যারামিটার।


৩. useParams দিয়ে quizId রিড করতে:

QuizPage-এ React Router-এর useParams হুক ব্যবহার করে আমরা quizId পেতে পারি। উদাহরণ:

import { useParams } from "react-router-dom";
 
export default function QuizPage() {
  const { quizId } = useParams(); // quizId URL থেকে পাওয়া যাবে।
 
  return (
    <main className="max-w-8xl mx-auto h-[calc(100vh-10rem)]">
      <h1>Quiz ID: {quizId}</h1>
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-10 h-full">
        <div className="lg:col-span-1 bg-white rounded-md p-6 h-full flex flex-col">
          <QuizHeader />
          <QuizInfo />
          <UserAvatar />
        </div>
        <div className="lg:col-span-2 bg-white">
          <Question />
        </div>
      </div>
    </main>
  );
}

এখানে useParams React Router থেকে সরাসরি quizId এক্সট্র্যাক্ট করে।


৪. ডেটা পাঠানোর পুরো প্রক্রিয়া:

  1. QuizCard-এ একটি ডাইনামিক লিংক তৈরি করা হয়, যা quizId সহ QuizPage-এ রিডাইরেক্ট করে।
  2. React Router-এ /quiz/:quizId রুট সেটআপ করা হয়।
  3. QuizPage useParams ব্যবহার করে URL থেকে quizId পায় এবং সেই quizId ব্যবহার করে ডেটা বা API কল করে।

উদাহরণ সংক্ষেপে:

QuizCard:

<Link to={`/quiz/${quizId}`}>
  <div className="quiz-card">
    <h1>{title}</h1>
  </div>
</Link>

App.js:

<Routes>
  <Route path="/quiz/:quizId" element={<QuizPage />} />
</Routes>

QuizPage:

const { quizId } = useParams();
console.log(quizId); // URL থেকে quizId পেয়ে যাবে।

কেন এই পদ্ধতি ভালো?

  • Decoupled Architecture: QuizCard এবং QuizPage সরাসরি সংযুক্ত নয়, শুধুমাত্র URL-এর মাধ্যমে ডেটা আদান-প্রদান হয়।
  • Reusable Components: QuizCard এবং QuizPage একে অপরের উপর নির্ভরশীল না থাকায় সহজে পুনর্ব্যবহারযোগ্য।
  • URL Parameters: রাউট প্যারামিটার ব্যবহার করে ডেটা সহজে এক পেজ থেকে অন্য পেজে পাঠানো যায়।

এটি একটি ক্লিন এবং রিয়্যাক্টিভ উপায় ডেটা শেয়ার করার জন্য।

But We don't want to display the quizId in url.

It concerns the sever. instead of this: http://localhost:5173/quiz/3ac55a23-848d-486e-b820-d2c9a24bc62c, we can use only: http://localhost:5173/quiz/ যদি আপনি quizId URL-এ প্রদর্শন না করতে চান এবং শুধুমাত্র http://localhost:5173/quiz/ ব্যবহার করতে চান, তবে ডেটা পাঠানোর জন্য React Router-এর state প্রপার্টি ব্যবহার করতে পারেন। এই পদ্ধতিতে quizId সরাসরি URL-এ দেখাবে না, বরং Link কম্পোনেন্টের মাধ্যমে state প্রপার্টিতে পাঠানো হবে এবং QuizPage-এ এটি অ্যাক্সেস করা যাবে।


এই পদ্ধতির ধাপসমূহ:


১. Link এর মাধ্যমে ডেটা পাঠানো (State ব্যবহার করে):

Link কম্পোনেন্টের state প্রপার্টিতে quizId সহ প্রয়োজনীয় ডেটা পাঠাতে পারেন। উদাহরণ:

<Link
  to="/quiz"
  state={{ quizId }}
  className="rounded-lg overflow-hidden shadow-lg hover:shadow-xl transition-shadow max-h-[450px] relative group cursor-pointer"
>
  <div className="group-hover:scale-105 absolute transition-all text-white  text-center top-1/2 -translate-y-1/2 px-4">
    <h1 className=" text-5xl font-jaro">{title}</h1>
    <p className="mt-2 text-lg">{description}</p>
  </div>
  <img
    src={thumbnail}
    alt={title}
    className="w-full h-full object-cover rounded mb-4"
  />
</Link>

এখানে to="/quiz" রুট ঠিক থাকে, কিন্তু quizId state প্রপার্টির মাধ্যমে পাঠানো হয়।


২. useLocation দিয়ে ডেটা রিসিভ করা:

QuizPage-এ useLocation হুক ব্যবহার করে state থেকে ডেটা অ্যাক্সেস করা যায়। উদাহরণ:

import { useLocation } from "react-router-dom";
 
export default function QuizPage() {
  const location = useLocation();
  const { quizId } = location.state || {}; // State থেকে quizId পাওয়া যাচ্ছে
 
  return (
    <main className="max-w-8xl mx-auto h-[calc(100vh-10rem)]">
      <h1>Quiz ID: {quizId}</h1>
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-10 h-full">
        <div className="lg:col-span-1 bg-white rounded-md p-6 h-full flex flex-col">
          <QuizHeader />
          <QuizInfo />
          <UserAvatar />
        </div>
        <div className="lg:col-span-2 bg-white">
          <Question />
        </div>
      </div>
    </main>
  );
}

এখানে:

  • useLocation হুক URL-এর সাথে পাঠানো state এক্সেস করে।
  • quizId অ্যাক্সেস করতে location.state.quizId ব্যবহার করা হয়।

৩. Route সেটআপ:

React Router-এ /quiz রুট কনফিগার করতে হবে। এখানে quizId URL-এ নেই, শুধুমাত্র state প্রপার্টিতে থাকে:

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import QuizPage from "./QuizPage";
import QuizCard from "./QuizCard";
 
function App() {
  return (
    <Router>
      <Routes>
        <Route path="/quiz" element={<QuizPage />} />
        <Route path="/" element={<QuizCard />} />
      </Routes>
    </Router>
  );
}
 
export default App;

কেন state প্রপার্টি ব্যবহার করবেন?

  1. URL পরিষ্কার রাখে: quizId URL-এ দেখানো হয় না, যা সিকিউরিটির জন্য ভালো।
  2. Server Concerns: যেহেতু quizId ক্লায়েন্ট-সাইডে থাকে, এটি সার্ভার সম্পর্কিত কোনো সমস্যা তৈরি করে না।
  3. Dynamic Data Passing: state প্রপার্টির মাধ্যমে যেকোনো ডেটা ডাইনামিকভাবে পাঠানো যায়।

ডেটা পাঠানোর পুরো প্রক্রিয়া:

  1. QuizCard থেকে Link-এর state প্রপার্টিতে quizId সহ ডেটা পাঠানো হয়।
  2. React Router-এ /quiz রুট সেটআপ করা হয়।
  3. QuizPage-এ useLocation ব্যবহার করে state থেকে quizId রিসিভ করা হয়।

উদাহরণ সংক্ষেপে:

QuizCard:

<Link to="/quiz" state={{ quizId }}>
  <div className="quiz-card">
    <h1>{title}</h1>
  </div>
</Link>

App.js:

<Routes>
  <Route path="/quiz" element={<QuizPage />} />
</Routes>

QuizPage:

import { useLocation } from "react-router-dom";
 
const { quizId } = useLocation().state || {};
console.log(quizId); // Quiz ID এখানে পাওয়া যাবে।

এটি একটি ক্লিন ও সিকিউর পদ্ধতি যেখানে quizId URL-এ প্রকাশিত হয় না।

Imagine there are questions of array. How we can display one by one question with next button. and display how many question remaining and how many Question participated.

To achieve this functionality, you can create a simple React application. Here’s how you can structure it:

1. Setup State for Questions

You need to track:

  • The current question index.
  • How many questions are remaining.
  • How many questions the user has participated in.

2. State Variables

Define these states:

  • questions: An array of questions.
  • currentIndex: The index of the current question.
  • participatedCount: Number of questions participated in.

3. Next Button Functionality

When the "Next" button is clicked:

  • Increment the currentIndex.
  • Update participatedCount.

4. Example Code

Here’s a simple implementation:

import React, { useState } from "react";
 
const QuizApp = () => {
  const questions = [
    "What is the capital of Bangladesh?",
    "What is 2 + 2?",
    "Who wrote the novel '1984'?",
    "What is the color of the sky?",
  ];
 
  const [currentIndex, setCurrentIndex] = useState(0);
  const [participatedCount, setParticipatedCount] = useState(0);
 
  const handleNext = () => {
    if (currentIndex < questions.length - 1) {
      setCurrentIndex(currentIndex + 1);
      setParticipatedCount(participatedCount + 1);
    } else {
      alert("You've reached the end of the questions!");
    }
  };
 
  return (
    <div style={{ padding: "20px", textAlign: "center" }}>
      <h2>Quiz App</h2>
      <div>
        <p>
          <strong>Question:</strong> {questions[currentIndex]}
        </p>
        <p>
          <strong>Remaining:</strong> {questions.length - (currentIndex + 1)}
        </p>
        <p>
          <strong>Participated:</strong> {participatedCount}
        </p>
      </div>
      <button onClick={handleNext} style={{ marginTop: "10px" }}>
        Next
      </button>
    </div>
  );
};
 
export default QuizApp;

5. How It Works

  1. Initial States:
    • currentIndex starts at 0.
    • participatedCount starts at 0.
  2. Display Questions:
    • The current question is shown based on currentIndex.
  3. Update States on Next:
    • handleNext increments currentIndex and participatedCount.

6. Output

  • Displays the current question.
  • Shows how many questions are remaining.
  • Tracks how many questions the user has participated in.

You can further enhance this by adding features like:

  • Showing answers.
  • Allowing users to go back to previous questions.
  • Tracking scores.

অপশনগুলোর অবস্থান এলোমেলোভাবে পরিবর্তন করবেন কীভাবে?

প্রতিটি প্রশ্নের অপশনগুলো এলোমেলোভাবে দেখানোর জন্য আপনাকে একটি শাফল (shuffle) ফাংশন ব্যবহার করতে হবে। এই ফাংশন অপশনগুলোর অ্যারে (array) এলোমেলোভাবে সাজিয়ে দেবে। নিচে এটি করার পদ্ধতি দেখানো হলো:


১. শাফল ফাংশন

প্রথমে একটি ফাংশন তৈরি করুন, যা অপশনগুলোর অ্যারে এলোমেলোভাবে সাজাবে:

const shuffleArray = (array) => {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1)); // এলোমেলো ইনডেক্স
    [array[i], array[j]] = [array[j], array[i]]; // উপাদান পরিবর্তন
  }
  return array;
};

২. কোডে শাফল যুক্ত করা

আপনার Question কম্পোনেন্টে অপশনগুলো শাফল করার জন্য shuffleArray ফাংশন ব্যবহার করুন।

আপডেটেড কোড:

import { useState, useEffect } from "react";
import NextButton from "./NextButton";
import Options from "./Options";
 
function shuffleArray(array) {
  const shuffled = [...array]; // অ্যারেটির কপি তৈরি করুন, যাতে মূল ডেটা পরিবর্তন না হয়
  for (let i = shuffled.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1)); // এলোমেলো ইনডেক্স
    [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; // উপাদান পরিবর্তন
  }
  return shuffled;
}
 
export default function Question({ questions = [] }) {
  const [currentIndex, setCurrentIndex] = useState(0);
  const [participated, setParticipated] = useState(0);
  const [shuffledOptions, setShuffledOptions] = useState([]);
 
  useEffect(() => {
    if (questions[currentIndex]?.options) {
      // প্রশ্ন পরিবর্তন হলে অপশনগুলো শাফল করুন
      setShuffledOptions(shuffleArray(questions[currentIndex].options));
    }
  }, [currentIndex, questions]);
 
  function handleNext() {
    if (currentIndex < questions.length - 1) {
      setCurrentIndex(currentIndex + 1);
      setParticipated(participated + 1);
    } else {
      console.log("কুইজ শেষ");
    }
  }
 
  const currentQuestion = questions[currentIndex];
 
  return (
    <div className="bg-white p-6 rounded-md">
      <div className="flex justify-between items-center mb-4">
        <h3 className="text-2xl font-semibold">
          {currentQuestion?.question || "কোন প্রশ্ন নেই"}
        </h3>
        <p>
          <strong>বাকি প্রশ্ন:</strong> {questions.length - (currentIndex + 1)}
          {" | "}
          <strong>উত্তর দিয়েছেন:</strong> {participated}
        </p>
      </div>
      {/* অপশনগুলো শাফল করার পর Options কম্পোনেন্টে পাঠানো */}
      <Options options={shuffledOptions} />
      <NextButton
        onNext={handleNext}
        disabled={currentIndex >= questions.length - 1}
      />
    </div>
  );
}

৩. Options কম্পোনেন্ট আপডেট

Options কম্পোনেন্ট যেন শাফল করা অপশনগুলো সঠিকভাবে দেখায় তা নিশ্চিত করুন:

export default function Options({ options = [] }) {
  return (
    <div className="mb-4">
      {options.length > 0 ? (
        <ul className="list-disc ml-6">
          {options.map((option, index) => (
            <li key={index} className="mb-2">
              {option}
            </li>
          ))}
        </ul>
      ) : (
        <p className="text-gray-500">কোন অপশন নেই</p>
      )}
    </div>
  );
}

কিভাবে এটি কাজ করে:

  1. শাফলিং (Shuffling):

    • যখনই currentIndex পরিবর্তন হবে, useEffect চালু হবে এবং অপশনগুলো এলোমেলোভাবে সাজাবে।
  2. মূল ডেটা অক্ষুণ্ণ রাখা:

    • questions অ্যারে পরিবর্তন না করে, কপি তৈরি করে শাফল করা হয়।
  3. অপশন আপডেট:

    • শাফল করা অপশন Options কম্পোনেন্টে পাঠানো হয়।

উদাহরণ ইনপুট:

const questionsArray = [
  {
    question: "বাংলাদেশের রাজধানী কী?",
    options: ["ঢাকা", "চট্টগ্রাম", "খুলনা", "সিলেট"],
  },
  {
    question: "২ + ২ কত?",
    options: ["৩", "৪", "৫", "৬"],
  },
];

উদাহরণ আউটপুট:

প্রথম প্রশ্নের জন্য অপশনগুলো এলোমেলোভাবে হতে পারে:

  • "সিলেট", "ঢাকা", "খুলনা", "চট্টগ্রাম"

দ্বিতীয় প্রশ্নের জন্য:

  • "৫", "৪", "৩", "৬"

প্রতিবার প্রশ্ন লোড করার সময় অপশনগুলোর ক্রম এলোমেলোভাবে পরিবর্তিত হবে।

নিচের কোডটি ব্যাখ্যা করা হচ্ছে, যেখানে Checkbox ব্যবহার করা হয়েছে, তবে এর কার্যকারিতা Radio Button-এর মতো। অর্থাৎ, একটি অপশন নির্বাচন করার পর এটি আনডু (অপসারণ) করা যাবে না, কিন্তু অন্য অপশন সিলেক্ট করা যাবে।


ইউজারের উত্তর সংগ্রহ এবং সাবমিট করা, রি-ডাইরেক্ট করে রেজাল্ট পেজে সঠিক ও ভুল উত্তর প্রদর্শন

১. কোড কাঠামো এবং ফাংশনালিটি ব্যাখ্যা

আমাদের কোডে মোটামুটি তিনটি প্রধান কম্পোনেন্ট রয়েছে:

  1. Question.jsx: এটি মূল প্রশ্ন দেখানোর কম্পোনেন্ট।
  2. Options.jsx: এটি অপশনগুলো দেখানোর কম্পোনেন্ট।
  3. NextButton.jsx: এটি পরবর্তী প্রশ্নে যাওয়ার জন্য বাটন।

২. কোড আপডেট এবং নতুন ফিচার যুক্ত করা

২.১. প্রশ্নের অপশন নির্বাচন এবং উত্তর সংগ্রহ

প্রত্যেক প্রশ্নের জন্য অপশন নির্বাচন করা এবং তা সংগ্রহ করা দরকার। আমরা useState হুকে প্রতিটি প্রশ্নের জন্য ব্যবহারকারীর নির্বাচিত উত্তর সংরক্ষণ করব।

import { useEffect, useState } from "react";
import shuffleArray from "../../utils/shuffleArray";
import Options from "./Options";
 
export default function Question({ questions = [] }) {
  const [currentIndex, setCurrentIndex] = useState(0);
  const [participated, setParticipated] = useState(0);
  const [shuffledOptions, setShuffledOptions] = useState([]);
  const [answers, setAnswers] = useState({}); // উত্তর সংরক্ষণের জন্য স্টেট
 
  useEffect(() => {
    if (questions[currentIndex]?.options) {
      setShuffledOptions(shuffleArray(questions[currentIndex].options));
    }
  }, [currentIndex, questions]);
 
  function handleNext() {
    if (currentIndex < questions.length - 1) {
      setCurrentIndex(currentIndex + 1);
      setParticipated(participated + 1);
    } else {
      handleSubmit();
    }
  }
 
  function handleAnswer(questionId, selectedAnswer) {
    setAnswers({
      ...answers,
      [questionId]: selectedAnswer,
    });
  }
 
  async function handleSubmit() {
    const quizId = "3ac55a23-848d-486e-b820-d2c9a24bc62c"; // আপনার কুইজ ID
    const data = { answers };
    try {
      const response = await api.post(
        `${import.meta.env.VITE_SERVER_URL}/api/quizzes/${quizId}/attempt`,
        data
      );
      console.log("Quiz submitted", response.data);
      // রেজাল্ট পেজে নিয়ে যাওয়া
      window.location.href = "/result";
    } catch (error) {
      console.error("Error submitting quiz", error);
    }
  }
 
  const currentQuestion = questions[currentIndex];
 
  return (
    <div className="bg-white p-6 rounded-md">
      <div className="flex justify-between items-center mb-4">
        <h3 className="text-2xl font-semibold">
          Q{currentIndex + 1}.{" "}
          {currentQuestion?.question || "No question available"}
        </h3>
      </div>
      <p className="text-xs text-gray-500 mb-4">
        Remaining: {questions.length - (currentIndex + 1)} | Participated:{" "}
        {participated}
      </p>
      <Options
        options={shuffledOptions}
        onNext={handleNext}
        currentIndex={currentIndex}
        questions={questions}
        onAnswer={handleAnswer}
      />
    </div>
  );
}

২.২. অপশন নির্বাচন ফাংশনালিটি (Options.jsx এবং Option.jsx)

এখানে আমরা onAnswer ফাংশনটি ব্যবহার করে নির্বাচিত অপশনের মান সংগ্রহ করব।

Options.jsx

import NextButton from "./NextButton";
import Option from "./Option";
 
export default function Options({
  options = [],
  onNext,
  currentIndex,
  questions,
  onAnswer,
}) {
  return (
    <>
      <div className="grid grid-cols-2 gap-4">
        {options.map((option, index) => (
          <Option
            key={index}
            name={option}
            option={option}
            onAnswer={onAnswer}
            questionId={questions[currentIndex].id}
          />
        ))}
      </div>
      <NextButton
        onNext={onNext}
        currentIndex={currentIndex}
        totalQ={questions?.length}
      />
    </>
  );
}

Option.jsx

import { useState } from "react";
 
export default function Option({ name, option, onAnswer, questionId }) {
  const [isSelected, setIsSelected] = useState(false);
 
  const handleChange = () => {
    setIsSelected(true);
    onAnswer(questionId, option);
  };
 
  return (
    <label className="flex items-center space-x-3 py-3 px-4 bg-primary/5 rounded-md text-lg">
      <input
        type="checkbox"
        name={name}
        className="form-checkbox text-buzzr-purple"
        checked={isSelected}
        onChange={handleChange}
        disabled={isSelected}
      />
      <span>{option}</span>
    </label>
  );
}

২.৩. Circular Progress Bar ইনস্টল এবং ব্যবহার

আমরা react-circular-progressbar লাইব্রেরি ব্যবহার করব।

npm install react-circular-progressbar

Result.jsx

import { CircularProgressbar } from "react-circular-progressbar";
import "react-circular-progressbar/dist/styles.css";
 
export default function Result({ resultData }) {
  const { percentage, correct_answers, submitted_answers } = resultData;
 
  return (
    <div className="result-page">
      <h2>Quiz Result</h2>
      <div style={{ width: 200, height: 200 }}>
        <CircularProgressbar value={percentage} text={`${percentage}%`} />
      </div>
      <h3>Score: {percentage}%</h3>
      <h4>Correct Answers: {correct_answers.length}</h4>
      <h4>
        Incorrect Answers: {submitted_answers.length - correct_answers.length}
      </h4>
      <div className="answers">
        {submitted_answers.map((answer, index) => (
          <div key={index} className="answer">
            <p>Question ID: {answer.question_id}</p>
            <p>Your Answer: {answer.answer}</p>
            <p>
              Correct Answer:{" "}
              {
                correct_answers.find(
                  (ca) => ca.question_id === answer.question_id
                )?.answer
              }
            </p>
          </div>
        ))}
      </div>
    </div>
  );
}

৩. ডকুমেন্টেশন

৩.১. কোডের বিভিন্ন অংশের ব্যাখ্যা

  1. Question.jsx:

    • useEffect: প্রশ্ন পরিবর্তন হলে অপশনগুলো শাফল করা হয়।
    • handleNext: পরবর্তী প্রশ্নে যাওয়ার ফাংশন।
    • handleAnswer: নির্বাচিত উত্তর সংগ্রহ করার ফাংশন।
    • handleSubmit: সমস্ত উত্তর সার্ভারে পাঠানোর ফাংশন।
  2. Options.jsx:

    • onAnswer: নির্বাচিত অপশন সংগ্রহ করার জন্য onAnswer ফাংশন ব্যবহার করা হয়।
  3. Option.jsx:

    • useState: অপশন নির্বাচন করা হয়েছে কিনা তা সংরক্ষণ করা হয়।
    • handleChange: অপশন নির্বাচন করার ফাংশন।
  4. Result.jsx:

    • CircularProgressbar: ব্যবহারকারীর স্কোর প্রদর্শনের জন্য।

৩.২. ফিচার এবং ফাংশনালিটি

  1. প্রশ্নের অপশন নির্বাচন এবং উত্তর সংগ্রহ।
  2. পরবর্তী প্রশ্নে যাওয়া এবং কুইজ সম্পন্ন হলে উত্তর সার্ভারে পাঠানো।
  3. রেজাল্ট পেজে সঠিক এবং ভুল উত্তর প্রদর্শন।
  4. Circular Progress Bar ব্যবহার করে ব্যবহারকারীর পারফরমেন্স গ্রাফিক্যালি দেখানো।

৪. উপসংহার

এই ডকুমেন্টেশন এবং কোড আপডেটের মাধ্যমে, আমরা কুইজ কম্পোনেন্টের ফিচার এবং ফাংশনালিটি সফলভাবে যুক্ত করেছি এবং কুইজের রেজাল্ট পেজে ব্যবহারকারীর পারফরমেন্স দেখানোর ব্যবস্থা করেছি।

নিচে উল্লেখ করা সমস্যাগুলো সমাধান করার জন্য আমরা কোড আপডেট করব এবং প্রতিটি ধাপ বাংলায় ব্যাখ্যা করব।


সমস্যা ১:

ব্যবহারকারী একাধিক অপশন সিলেক্ট করতে পারছেন, কিন্তু আমরা চাই ব্যবহারকারী শুধুমাত্র একটি অপশন সিলেক্ট করতে পারবেন। একবার একটি অপশন সিলেক্ট করার পর, অন্য অপশন সিলেক্ট করার সুযোগ থাকবে না।

সমাধান:
আমরা Options এবং Option কম্পোনেন্ট আপডেট করব, যাতে একবার একটি অপশন সিলেক্ট হলে বাকিগুলো নিষ্ক্রিয় (disabled) হয়ে যায়।


সমস্যা ২:

নতুন প্রশ্ন এবং অপশন আসার পরেও আগের সিলেক্টেড অপশন চেকড থাকে। নতুন প্রশ্ন আসলে এই সিলেক্টেড অপশনগুলো রিসেট হওয়া উচিত।

সমাধান:
আমরা Options কম্পোনেন্ট এবং Option কম্পোনেন্ট আপডেট করব, যাতে নতুন প্রশ্ন লোড হলে সিলেক্টেড অপশনগুলো রিসেট হয়।


ধাপে ধাপে কোড আপডেট ও ব্যাখ্যা

ধাপ ১: Options.jsx এ স্টেট যোগ করা

আমরা Options কম্পোনেন্টে একটি স্টেট যোগ করব, যা কন্ট্রোল করবে কোন অপশন সিলেক্ট হয়েছে।

import { useState } from "react";
import NextButton from "./NextButton";
import Option from "./Option";
 
export default function Options({
  options = [],
  onNext,
  currentIndex,
  questions,
  onAnswer,
}) {
  const [selectedOption, setSelectedOption] = useState(null); // সিলেক্টেড অপশন ট্র্যাক করবে
 
  function handleOptionSelect(option) {
    setSelectedOption(option); // সিলেক্টেড অপশন সেট করা
    onAnswer(questions[currentIndex]?.id, option); // প্যারেন্টে উত্তর পাঠানো
  }
 
  return (
    <>
      <div className="grid grid-cols-2 gap-4">
        {options.map((option, index) => (
          <Option
            key={index}
            name={option}
            option={option}
            isDisabled={!!selectedOption} // একবার সিলেক্ট হলে বাকিগুলো নিষ্ক্রিয়
            isSelected={selectedOption === option} // চেকড স্টেট
            onSelect={() => handleOptionSelect(option)}
          />
        ))}
      </div>
      <NextButton
        onNext={() => {
          setSelectedOption(null); // নতুন প্রশ্নে যাওয়ার সময় রিসেট
          onNext();
        }}
        currentIndex={currentIndex}
        totalQ={questions?.length}
      />
    </>
  );
}

ধাপ ২: Option.jsx আপডেট করা

Option কম্পোনেন্টে নতুন প্রপস (isDisabled, isSelected) হ্যান্ডল করব।

export default function Option({
  name,
  option,
  isDisabled,
  isSelected,
  onSelect,
}) {
  return (
    <label
      className={`flex items-center space-x-3 py-3 px-4 rounded-md text-lg ${
        isSelected ? "bg-primary text-white" : "bg-primary/5"
      }`}
    >
      <input
        type="radio" // চেকবক্স পরিবর্তন করে রেডিও বাটন করা
        name={name}
        checked={isSelected} // চেকড অবস্থা
        disabled={isDisabled} // নিষ্ক্রিয় অবস্থা
        onChange={onSelect} // সিলেক্ট করা হ্যান্ডলিং
        className="form-radio text-buzzr-purple"
      />
      <span>{option}</span>
    </label>
  );
}

ধাপ ৩: handleNext ফাংশনে রিসেট যোগ করা

handleNext ফাংশনে সিলেক্টেড অপশন রিসেট করার ব্যবস্থা করব। এটি Options এ করা হলেও নিশ্চিত করার জন্য Question কম্পোনেন্টেও এ ফাংশন নিশ্চিত করব।

function handleNext() {
  if (currentIndex < questions.length - 1) {
    setCurrentIndex(currentIndex + 1);
    setParticipated(participated + 1);
  } else {
    handleSubmit();
  }
}

ধাপ ৪: কোড টেস্ট করা

আপডেটেড কোডে নিচের দুটি ফিচার যুক্ত হয়েছে:

  1. একবার একটি অপশন সিলেক্ট হলে বাকিগুলো নিষ্ক্রিয় হবে।
  2. নতুন প্রশ্ন লোড হলে সিলেক্টেড অপশনগুলো রিসেট হবে।

আপডেটেড কোডের সুবিধা

  • ব্যবহারকারী শুধুমাত্র একটি অপশন সিলেক্ট করতে পারবেন।
  • নতুন প্রশ্ন লোড হলে আগের সিলেক্টেড অপশন রিসেট হবে।

যদি কোনও অংশে আরও উন্নয়নের প্রয়োজন হয়, জানাতে ভুলবেন না! 😊

Result Page Implementation

আপনার কোডকে ছোট ছোট উপাদানে ভাগ করা এবং পুনরায় ব্যবহারযোগ্য করা অবশ্যই ভালো অভ্যাস। এখানে /result পেজের জন্য কিভাবে আপনার কোডকে বিভিন্ন কম্পোনেন্টে ভাগ করা যায় তা দেখানো হলো:

১. ResultPage.js

এই কম্পোনেন্টটি সকল অন্যান্য কম্পোনেন্টকে একত্রিত করে।

import React from "react";
import Logo from "./Logo";
import QuizSummary from "./QuizSummary";
import QuestionList from "./QuestionList";
 
const ResultPage = () => {
  return (
    <div className="flex min-h-screen overflow-hidden">
      <Logo />
      <QuizSummary />
      <QuestionList />
    </div>
  );
};
 
export default ResultPage;

২. Logo.js

এই কম্পোনেন্টটি লোগো প্রদর্শনের জন্য।

import React from "react";
 
const Logo = () => {
  return (
    <img
      src="./assets/logo-white.svg"
      className="max-h-11 fixed left-6 top-6 z-50"
      alt="Logo"
    />
  );
};
 
export default Logo;

৩. QuizSummary.js

এই কম্পোনেন্টটি বাম পাশে কুইজের সারাংশ প্রদর্শন করে।

import React from "react";
import { useNavigate } from "react-router-dom";
 
const QuizSummary = () => {
  const navigate = useNavigate();
 
  return (
    <div className="max-h-screen overflow-hidden hidden lg:flex lg:w-1/2 bg-primary flex-col justify-center p-12 relative">
      <div>
        <div className="text-white">
          <div>
            <h2 className="text-4xl font-bold mb-2">React Hooks Quiz</h2>
            <p>
              A quiz on React hooks like useState, useEffect, and useContext.
            </p>
          </div>
 
          <div className="my-6 flex items-center">
            <div className="w-1/2">
              <div className="flex gap-6 my-6">
                <div>
                  <p className="font-semibold text-2xl my-0">10</p>
                  <p className="text-gray-300">Questions</p>
                </div>
 
                <div>
                  <p className="font-semibold text-2xl my-0">8</p>
                  <p className="text-gray-300">Correct</p>
                </div>
 
                <div>
                  <p className="font-semibold text-2xl my-0">2</p>
                  <p className="text-gray-300">Wrong</p>
                </div>
              </div>
 
              <button
                onClick={() => navigate("/leaderboard_page")}
                className="bg-secondary py-3 rounded-md hover:bg-secondary/90 transition-colors text-lg font-medium underline text-white"
              >
                View Leaderboard
              </button>
            </div>
 
            <div className="w-1/2 bg-primary/80 rounded-md border border-white/20 flex items-center p-4">
              <div className="flex-1">
                <p className="text-2xl font-bold">5/10</p>
                <p>Your Mark</p>
              </div>
              <div>
                <img
                  src="./assets/icons/circular-progressbar.svg"
                  className="h-20"
                  alt="Progress"
                />
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};
 
export default QuizSummary;

৪. QuestionList.js

এই কম্পোনেন্টটি প্রশ্নগুলির তালিকা প্রদর্শন করে।

import React from "react";
import Question from "./Question";
 
const QuestionList = () => {
  const questions = [
    {
      id: 1,
      text: "Which of the following is NOT a binary tree traversal method?",
      options: ["Inorder", "Preorder", "Postorder", "Crossorder"],
      correctAnswer: "Crossorder",
      userAnswer: "Inorder",
    },
    {
      id: 2,
      text: "What is the maximum number of nodes at level 'L' in a binary tree?",
      options: ["2^L", "L", "2^(L-1)", "2L"],
      correctAnswer: "2^L",
      userAnswer: "2^L",
    },
    {
      id: 3,
      text: "What is the height of an empty binary tree?",
      options: ["0", "-1", "1", "Undefined"],
      correctAnswer: "0",
      userAnswer: "0",
    },
  ];
 
  return (
    <div className="max-h-screen md:w-1/2 flex items-center justify-center h-full p-8">
      <div className="h-[calc(100vh-50px)] overflow-y-scroll">
        <div className="px-4">
          {questions.map((question) => (
            <Question key={question.id} question={question} />
          ))}
        </div>
      </div>
    </div>
  );
};
 
export default QuestionList;

৫. Question.js

এই কম্পোনেন্টটি একটি নির্দিষ্ট প্রশ্ন প্রদর্শন করে।

import React from "react";
 
const Question = ({ question }) => {
  return (
    <div className="rounded-lg overflow-hidden shadow-sm mb-4">
      <div className="bg-white p-6 !pb-2">
        <div className="flex justify-between items-center mb-4">
          <h3 className="text-lg font-semibold">
            {question.id}. {question.text}
          </h3>
        </div>
        <div className="space-y-2">
          {question.options.map((option, index) => (
            <label key={index} className="flex items-center space-x-3">
              <input
                type="radio"
                name={`answer${question.id}`}
                className="form-radio text-buzzr-purple"
                defaultChecked={option === question.userAnswer}
                disabled
              />
              <span>{option}</span>
            </label>
          ))}
        </div>
      </div>
      <div className="flex space-x-4 bg-primary/10 px-6 py-2">
        <button className="text-red-600 hover:text-red-800 font-medium">
          Delete
        </button>
        <button className="text-primary hover:text-primary/80 font-medium">
          Edit Question
        </button>
      </div>
    </div>
  );
};
 
export default Question;

এই পদ্ধতিতে, প্রতিটি কম্পোনেন্ট নির্দিষ্ট একটি অংশের জন্য দায়ী, যা পরিচালনা ও রক্ষণাবেক্ষণ করা সহজ করে। আপনি প্রয়োজনে আরও কম্পোনেন্ট ভাগ করতে পারেন বা বৈশিষ্ট্য যোগ করতে পারেন।

How implement a logic here. to height header and footer. in "/result" route.

To conditionally hide the Header component and a potential Footer on the /result route while keeping them visible on other routes, you can use the useLocation hook from react-router-dom. This allows you to check the current route and apply conditional rendering logic.

Here’s the updated PrivateRoutes component:

import { Navigate, Outlet, useLocation } from "react-router-dom";
import Header from "../components/common/Header";
import { useAuth } from "../hooks/useAuth";
 
export default function PrivateRoutes() {
  const { auth } = useAuth();
  const location = useLocation();
 
  // Determine if the current route is "/result"
  const isResultRoute = location.pathname === "/result";
 
  return (
    <>
      {auth?.user ? (
        <div className="container">
          {/* Conditionally render Header */}
          {!isResultRoute && <Header />}
          <main
            className={`mx-auto max-w-[1020px] ${
              isResultRoute ? "pt-0 pb-0" : "pt-4 pb-4"
            }`}
          >
            <div>
              <Outlet />
            </div>
          </main>
        </div>
      ) : (
        <Navigate to="/login" />
      )}
    </>
  );
}

Explanation:

  1. useLocation: This hook provides access to the current URL, allowing us to check if the route is /result.
  2. isResultRoute: A boolean variable checks if the current path matches /result.
  3. Conditional Rendering:
    • The Header component is rendered only if the route is not /result.
    • Adjust padding (pt-0 pb-0 for /result or default spacing otherwise) in the main element for consistent layout.

This approach keeps your logic clean and focused while ensuring the Header and Footer visibility adjusts dynamically based on the route. Let me know if you have a footer to integrate as well!

How We send response.data data to ResultPage. There is no relation between two components.

To send response.data from the Question component to the ResultPage component without a direct relationship between the two components, you can use one of the following methods:


1. Using Context API

You can create a context to share the data across components. This is particularly useful if the ResultPage and Question components are part of the same component tree.

Steps:

  1. Create a context (e.g., ResultContext):

    import React, { createContext, useState, useContext } from "react";
     
    const ResultContext = createContext();
     
    export function ResultProvider({ children }) {
      const [resultData, setResultData] = useState(null);
     
      return (
        <ResultContext.Provider value={{ resultData, setResultData }}>
          {children}
        </ResultContext.Provider>
      );
    }
     
    export function useResult() {
      return useContext(ResultContext);
    }
  2. Wrap your application (or a common parent component) with the ResultProvider:

    import { ResultProvider } from "./context/ResultContext";
    import Question from "./components/Question";
    import ResultPage from "./components/ResultPage";
     
    function App() {
      return (
        <ResultProvider>
          {/* Routes or Components */}
          <Question />
          <ResultPage />
        </ResultProvider>
      );
    }
    export default App;
  3. Use the useResult hook in Question to update the resultData:

    import { useResult } from "./../../context/ResultContext";
     
    function handleSubmit() {
      async function submitAnswers() {
        try {
          const response = await api.post(
            `${import.meta.env.VITE_SERVER_BASE_URL}/quizzes/${quizId}/attempt`,
            { answers }
          );
     
          if (response.status === 200) {
            console.log("Answer submitted successfully:", response.data);
            setResultData(response.data); // Save response to context
          }
        } catch (error) {
          console.error(
            "Error submitting answers:",
            error.response?.data || error
          );
        }
      }
      submitAnswers();
    }
  4. Use the useResult hook in ResultPage to access the resultData:

    import { useResult } from "./../../context/ResultContext";
     
    export default function ResultPage() {
      const { resultData } = useResult();
     
      return (
        <div>
          <h1>Result Page</h1>
          {resultData ? (
            <pre>{JSON.stringify(resultData, null, 2)}</pre>
          ) : (
            <p>No result data available.</p>
          )}
        </div>
      );
    }

2. Using a State Management Library (Redux, Zustand, etc.)

If your app uses a state management library, you can store the response.data in a global state and access it from the ResultPage.

Example with Zustand:

import create from "zustand";
 
const useResultStore = create((set) => ({
  resultData: null,
  setResultData: (data) => set({ resultData: data }),
}));
 
export default useResultStore;
  • In Question:

    import useResultStore from "./../../store/resultStore";
     
    const { setResultData } = useResultStore();
     
    function handleSubmit() {
      async function submitAnswers() {
        try {
          const response = await api.post(
            `${import.meta.env.VITE_SERVER_BASE_URL}/quizzes/${quizId}/attempt`,
            { answers }
          );
     
          if (response.status === 200) {
            console.log("Answer submitted successfully:", response.data);
            setResultData(response.data); // Store in Zustand
          }
        } catch (error) {
          console.error(
            "Error submitting answers:",
            error.response?.data || error
          );
        }
      }
      submitAnswers();
    }
  • In ResultPage:

    import useResultStore from "./../../store/resultStore";
     
    export default function ResultPage() {
      const { resultData } = useResultStore();
     
      return (
        <div>
          <h1>Result Page</h1>
          {resultData ? (
            <pre>{JSON.stringify(resultData, null, 2)}</pre>
          ) : (
            <p>No result data available.</p>
          )}
        </div>
      );
    }

3. Using Navigation State (React Router)

If you're navigating from Question to ResultPage, pass the response.data as state through React Router's useNavigate.

  • In Question:

    import { useNavigate } from "react-router-dom";
     
    const navigate = useNavigate();
     
    function handleSubmit() {
      async function submitAnswers() {
        try {
          const response = await api.post(
            `${import.meta.env.VITE_SERVER_BASE_URL}/quizzes/${quizId}/attempt`,
            { answers }
          );
     
          if (response.status === 200) {
            console.log("Answer submitted successfully:", response.data);
            navigate("/result", { state: { result: response.data } });
          }
        } catch (error) {
          console.error(
            "Error submitting answers:",
            error.response?.data || error
          );
        }
      }
      submitAnswers();
    }
  • In ResultPage:

    import { useLocation } from "react-router-dom";
     
    export default function ResultPage() {
      const location = useLocation();
      const resultData = location.state?.result;
     
      return (
        <div>
          <h1>Result Page</h1>
          {resultData ? (
            <pre>{JSON.stringify(resultData, null, 2)}</pre>
          ) : (
            <p>No result data available.</p>
          )}
        </div>
      );
    }

Choose the method that fits your application's structure and requirements. If you're using React Router, the navigation state might be the simplest solution. For more complex apps, Context API or a state management library like Zustand or Redux could be better.

How to two array compare each element equal or not and how many equal element, how many not equal element.

ওয়েব ডেভেলপমেন্টে for লুপ না মেথড, কোনটি বেশি প্রফেশনাল?

ওয়েব ডেভেলপমেন্টে for লুপ এবং অ্যারে মেথড (যেমন: forEach, map, filter)—দুটিই বৈধ এবং নির্ভরযোগ্য। তবে অ্যারে মেথড ব্যবহার করা বর্তমান সময়ের মডার্ন, সহজবোধ্য, এবং প্রফেশনাল পদ্ধতি হিসেবে বিবেচিত হয়।


তুলনা: for লুপ বনাম অ্যারে মেথড

১. for লুপ

গুণাবলি:

  • ইউজ করার সুবিধা: যে কোনো ধরণের লজিক প্রয়োগ করা যায় এবং প্রক্রিয়া মাঝপথে থামানো যায় (break বা return ব্যবহার করে)।
  • সামঞ্জস্যপূর্ণ: সবধরণের জাভাস্ক্রিপ্ট পরিবেশে কাজ করে।

দুর্বলতা:

  • কোড একটু বেশি বড় হয়।
  • জটিল কোডে ভুল হওয়ার ঝুঁকি বেশি থাকে।

উদাহরণ (for লুপ):

function compareArrays(arr1, arr2) {
  let equalCount = 0;
  let notEqualCount = 0;
 
  for (let i = 0; i < arr1.length; i++) {
    if (arr1[i] === arr2[i]) {
      equalCount++;
    } else {
      notEqualCount++;
    }
  }
 
  return { equalCount, notEqualCount };
}

২. অ্যারে মেথড

গুণাবলি:

  • সহজ: কোড পরিষ্কার ও বুঝতে সুবিধা।
  • ডিক্লারেটিভ: কাজ কী হবে তা বোঝা সহজ।
  • মডার্ন স্টাইল: বর্তমান প্রফেশনাল প্রোগ্রামিং পদ্ধতিতে বেশি ব্যবহৃত।

দুর্বলতা:

  • জটিল লজিকের ক্ষেত্রে ডিবাগ করা কঠিন হতে পারে।
  • লুপ মাঝপথে থামানো যায় না।

উদাহরণ (forEach):

function compareArrays(arr1, arr2) {
  let equalCount = 0;
  let notEqualCount = 0;
 
  arr1.forEach((value, index) => {
    if (value === arr2[index]) {
      equalCount++;
    } else {
      notEqualCount++;
    }
  });
 
  return { equalCount, notEqualCount };
}

উদাহরণ (reduce):

function compareArrays(arr1, arr2) {
  const result = arr1.reduce(
    (acc, value, index) => {
      if (value === arr2[index]) {
        acc.equalCount++;
      } else {
        acc.notEqualCount++;
      }
      return acc;
    },
    { equalCount: 0, notEqualCount: 0 }
  );
 
  return result;
}

কোনটি ব্যবহার করবেন?

অ্যারে মেথড ব্যবহার করুন যখন:

  • কোড সহজ ও ছোট রাখার চেষ্টা করছেন।
  • আপনি টিমে কাজ করছেন যেখানে ফাংশনাল প্রোগ্রামিং স্টাইল বেশি জনপ্রিয়।
  • জটিল লজিক ছাড়া সাধারণ কাজ করতে চান।

for লুপ ব্যবহার করুন যখন:

  • লুপের ওপর পূর্ণ নিয়ন্ত্রণ দরকার (যেমন: লুপ মাঝপথে থামানো)।
  • আপনি পারফরম্যান্স নিয়ে বেশি সচেতন (কিছু ক্ষেত্রে for লুপ একটু দ্রুততর)।
  • এমন পরিবেশে কাজ করছেন যেখানে অ্যারে মেথড সাপোর্ট করে না।

প্রফেশনাল পরামর্শ:

বর্তমান সময়ের ওয়েব ডেভেলপমেন্টে:

  1. সহজ এবং পরিষ্কার কাজের জন্য অ্যারে মেথড ব্যবহার করুন (যেমন: forEach, reduce)।
  2. for লুপ ব্যবহার করুন যদি জটিল লজিক বা বিশেষ কন্ট্রোল দরকার হয়।

আপনার উল্লেখিত টাস্কের জন্য, forEach বা reduce ব্যবহার করা প্রফেশনাল দেখায়, যদি বিশেষ কোনো কারণ না থাকে for লুপ ব্যবহারের।

send correct_answers, submitted_answer to store 'ansStore.js'. but mind it this ansStore data change, when correct_answers, submitted_answer changed. create ansStore.js we use zustaind.

Here's how you can create an ansStore.js using Zustand to store and update correct_answers and submitted_answers dynamically.

ansStore.js

import { create } from "zustand";
 
const useAnsStore = create((set) => ({
  correctAnswers: [],
  submittedAnswers: [],
 
  // Action to set correct and submitted answers
  setAnswers: (correct_answers, submitted_answers) =>
    set(() => ({
      correctAnswers: correct_answers,
      submittedAnswers: submitted_answers,
    })),
}));
 
export default useAnsStore;

Explanation:

  • useAnsStore: A Zustand store to manage correctAnswers and submittedAnswers.
  • setAnswers: An action to update the correctAnswers and submittedAnswers when correct_answers or submitted_answers changes.

Integrating With quizSummary

You can integrate the store with your quizSummary function to dynamically update the answers in the Zustand store. Here's the modified quizSummary function:

Updated quizSummary

import useAnsStore from "./ansStore";
 
export default function quizSummary(resultData, userId) {
  let correct_answer = 0;
  let wrong_answer = 0;
  let total_marks = 0;
  let user_marks = 0;
 
  const userAttempt = resultData?.attempts?.find(
    (attempt) => attempt?.user?.id === userId
  );
  const { correct_answers = [], submitted_answers = [] } = userAttempt;
 
  // Update the Zustand store with the latest answers
  const setAnswers = useAnsStore((state) => state.setAnswers);
  setAnswers(correct_answers, submitted_answers);
 
  console.log(correct_answers, submitted_answers);
 
  // Iterate through correct answers
  for (let i = 0; i < correct_answers.length; i++) {
    let isCorrect = false;
 
    // Loop through submitted answers
    for (let j = 0; j < submitted_answers.length; j++) {
      if (correct_answers[i].question_id === submitted_answers[j].question_id) {
        if (correct_answers[i].answer === submitted_answers[j].answer) {
          correct_answer++;
          user_marks += correct_answers[i].marks;
          isCorrect = true;
        }
        break;
      }
    }
 
    if (!isCorrect) {
      wrong_answer++;
    }
 
    total_marks += correct_answers[i].marks;
  }
 
  return {
    quizTitle: resultData.quiz?.title || "Unknown Quiz",
    quizDescription: resultData.quiz?.description || "No Description",
    totalQuestions: resultData.quiz?.total_questions || "0",
    correctAnswer: correct_answer,
    wrongAnswer: wrong_answer,
    userMarks: user_marks,
    totalMarks: total_marks,
    percentage:
      total_marks > 0 ? ((user_marks / total_marks) * 100).toFixed(1) : 0,
  };
}

Key Notes:

  1. Dynamic Updates: Whenever correct_answers or submitted_answers changes, the store is updated with setAnswers.
  2. Easy Access: Other components or functions can access these values from the Zustand store as needed:
    const { correctAnswers, submittedAnswers } = useAnsStore();

Let me know if you need further adjustments or explanations!

আগের উত্তর দেয়া থাকলে, আর সিলেক্ট করার সুযোগ দাও না।

আপনার কোডের উপর ভিত্তি করে ফিচারটি কিভাবে কাজ করবে, তা ধাপে ধাপে ব্যাখ্যা করা হলো:


১. answeredQuestions স্টেট যোগ করা

কোডে answeredQuestions নামে একটি স্টেট যোগ করা হয়েছে, যা প্রতিটি প্রশ্নের উত্তর ট্র্যাক করে।

const [answeredQuestions, setAnsweredQuestions] = useState({});
  • এটি একটি অবজেক্ট যেখানে প্রতিটি প্রশ্নের id অনুযায়ী উত্তর সংরক্ষিত থাকবে।
  • উদাহরণ: { "q1": "Option A", "q2": "Option B" }

২. useEffect দিয়ে উত্তর লোড করা

যখন ব্যবহারকারী Previous বা Next দিয়ে প্রশ্ন পরিবর্তন করবেন, তখন সেই প্রশ্নের আগে দেওয়া উত্তর দেখানোর জন্য useEffect ব্যবহার করা হয়েছে।

useEffect(() => {
  const currentQuestionId = questions[currentIndex]?.id;
  setSelectedOption(answeredQuestions[currentQuestionId] || null);
}, [currentIndex, answeredQuestions, questions]);
  • কাজ:
    • বর্তমান প্রশ্নের id দেখে উত্তর আছে কিনা চেক করে।
    • থাকলে সেটি selectedOption-এ সেট করে।
    • না থাকলে null রেখে দেয়।

৩. উত্তর নির্বাচন করা (handleOptionSelect)

যদি ব্যবহারকারী একটি অপশন নির্বাচন করেন এবং সেই প্রশ্নের উত্তর আগে না দেওয়া হয়ে থাকে, তাহলে সেটি answeredQuestions-এ সংরক্ষণ করা হয়।

function handleOptionSelect(option) {
  const currentQuestionId = questions[currentIndex]?.id;
 
  if (!answeredQuestions[currentQuestionId]) {
    setSelectedOption(option);
    setAnsweredQuestions((prev) => ({
      ...prev,
      [currentQuestionId]: option,
    }));
    onAnswer(currentQuestionId, option); // প্যারেন্ট কম্পোনেন্টে পাঠায়
  }
}
  • কাজ:
    • প্রশ্নের উত্তর দেওয়া হয়েছে কিনা চেক করে।
    • না দিলে উত্তর সংরক্ষণ করে এবং প্যারেন্টে (onAnswer) জানায়।

৪. উত্তর নির্বাচন বন্ধ রাখা (isDisabled)

যদি প্রশ্নের উত্তর দেওয়া হয়ে থাকে, তাহলে অপশনগুলো নিষ্ক্রিয় (disabled) করা হয়।

<Option
  key={index}
  name={option}
  option={option}
  isDisabled={!!answeredQuestions[questions[currentIndex]?.id]} // প্রশ্নের উত্তর দেওয়া আছে কিনা চেক
  isSelected={selectedOption === option} // নির্বাচন করা অপশন চিহ্নিত
  onSelect={() => handleOptionSelect(option)}
/>
  • কাজ:
    • প্রশ্নের উত্তর আগে দেওয়া থাকলে, ব্যবহারকারী নতুন করে উত্তর দিতে পারবেন না।
    • পূর্ববর্তী উত্তর হাইলাইট করা থাকে।

৫. Next বোতামে setSelectedOption রিসেট করা

পরবর্তী প্রশ্নে যাওয়ার সময় setSelectedOption(null) ব্যবহার করা হয়, যেন নতুন প্রশ্নে পূর্ববর্তী নির্বাচন না থাকে।

<NextButton
  onNext={() => {
    setSelectedOption(null); // নতুন প্রশ্নের জন্য নির্বাচন রিসেট
    onNext();
  }}
  currentIndex={currentIndex}
  totalQ={questions?.length}
/>

৬. Previous বোতামে উত্তর দেখানো

পূর্ববর্তী প্রশ্নে যাওয়ার সময় ব্যবহারকারী তার দেওয়া উত্তর দেখতে পারবেন। useEffect এই কাজটি করে।

<button
  onClick={onPrevious}
  className="w-1/3 text-center ml-auto block bg-primary text-white py-2 px-4 rounded-md hover:bg-indigo-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary mb-6 font-semibold my-8"
>
  Previous
</button>

সংক্ষিপ্ত সারসংক্ষেপ:

  • স্টেট ম্যানেজমেন্ট: answeredQuestions স্টেট উত্তর সংরক্ষণ করে।
  • উত্তর লক: উত্তর দেওয়া থাকলে অপশন নিষ্ক্রিয় থাকে।
  • নেভিগেশন: আগের প্রশ্নে গেলে উত্তর দেখানো হয় এবং নতুন প্রশ্নে রিসেট হয়।
  • ইউজার ফ্লো: উত্তর দেওয়া হলে তা চিহ্নিত করা হয় এবং পরিবর্তন করা যায় না।

এই ফিচারটি সহজেই কুইজ অ্যাপে প্রয়োগ করা যায় এবং ব্যবহারকারীর অভিজ্ঞতা উন্নত করে।