Untitled

mail@pastecode.io avatar
unknown
plain_text
2 months ago
9.1 kB
2
Indexable
Never
import {
  closestCenter,
  DndContext,
  MouseSensor,
  TouchSensor,
  useDraggable,
  useDroppable,
  useSensor,
  useSensors,
} from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities';
import DOMPurify from 'dompurify';
import { useState } from 'react';
import { IoClose } from 'react-icons/io5';
import { MdDragIndicator } from 'react-icons/md';
import { useRecoilValue } from 'recoil';
import { atomTestData } from '@/atoms/test-data';
import { useSectionStepNumberData } from '@/hooks/tests/test/section-step-number-data';
import { useTestUrlParams } from '@/hooks/tests/test/test-url-params';
import type { TTestQuestionOption } from '@/types/tests/test/test-question-option';
import { cn } from '@/utils/cn';

export function TypeDrangAndDrop() {
  const [activedTab, setActivedTab] = useState<'passage' | 'question'>(
    'question',
  );

  return (
    <section className="relative w-full">
      <SwitchTabBtn activedTab={activedTab} setActivedTab={setActivedTab} />
      <div>
        {activedTab === 'passage' && <TabPassage />}
        {activedTab === 'question' && <TabQuestion />}
      </div>
    </section>
  );
}

interface ISwitchTabBtnProps {
  activedTab: 'passage' | 'question';
  setActivedTab: (tab: 'passage' | 'question') => void;
}

const SwitchTabBtn = (props: ISwitchTabBtnProps) => {
  return (
    <div className="absolute -top-7 left-1/2 flex w-fit -translate-x-1/2 gap-1 rounded-xl border bg-white p-1.5">
      <button
        className={cn('px-2.5 py-[7px] transition-all text-nowrap rounded-lg', {
          'bg-purple text-white': props.activedTab === 'passage',
        })}
        onClick={() => props.setActivedTab('passage')}
      >
        View Passage
      </button>
      <button
        className={cn('px-2.5 py-[7px] transition-all text-nowrap rounded-lg', {
          'bg-purple text-white': props.activedTab === 'question',
        })}
        onClick={() => props.setActivedTab('question')}
      >
        View Question
      </button>
    </div>
  );
};

const TabPassage = () => {
  const sectionStepNumberData = useSectionStepNumberData();
  const testUrlParams = useTestUrlParams();
  const atomStateTestData = useRecoilValue(atomTestData);

  return (
    <div className="mt-6 flex flex-col gap-3 p-3">
      {/* Part title */}
      <p className="w-full text-center text-2xl font-bold">
        {atomStateTestData.parts[testUrlParams.sectionNumber]?.[0]?.name}
      </p>
      {/* Description */}
      <p
        dangerouslySetInnerHTML={{
          __html: DOMPurify.sanitize(sectionStepNumberData.content),
        }}
      />
    </div>
  );
};

const TabQuestion = () => {
  return (
    <div className="mt-9 flex flex-col gap-6">
      <TabQuestionTexts />
      <TabQuestionDragAndDrop />
    </div>
  );
};

const TabQuestionTexts = () => {
  const sectionStepNumberData = useSectionStepNumberData();

  return (
    <div className="flex flex-col gap-2">
      <p>
        <span className="text-lg font-bold">Direction : </span>
        <span
          dangerouslySetInnerHTML={{
            __html: DOMPurify.sanitize(
              String(sectionStepNumberData.question_direction_text),
            ),
          }}
        />
      </p>
      <p>
        <span className="text-lg font-bold">Question : </span>
        <span
          dangerouslySetInnerHTML={{
            __html: DOMPurify.sanitize(sectionStepNumberData.question_text),
          }}
        />
      </p>
    </div>
  );
};

const TabQuestionDragAndDrop = () => {
  return (
    <section>
      <DragDropContext />
    </section>
  );
};

interface IDraggableQuestionProps {
  data: TTestQuestionOption;
}

const DraggableQuestion = (props: IDraggableQuestionProps) => {
  const { attributes, listeners, setNodeRef, transform, isDragging } =
    useDraggable({
      id: props.data.id.toString(),
      data: props.data,
    });

  const style = {
    transform: CSS.Translate.toString(transform),
    transition: isDragging ? 'none' : 'transform 250ms ease',
  };

  return (
    <div
      ref={setNodeRef}
      style={style}
      {...listeners}
      {...attributes}
      className={cn(
        'min-h-20 w-full rounded-xl border border-gray-300 bg-white p-3 flex items-center gap-2',
        { 'opacity-50': isDragging },
      )}
    >
      <div className="flex-1">
        <MdDragIndicator className="size-8 text-purple/80" />
      </div>
      <span>{props.data.content}</span>
    </div>
  );
};

interface IDroppableAnswerProps {
  id: number;
}

const DroppableAnswer = (props: IDroppableAnswerProps) => {
  const { setNodeRef, isOver } = useDroppable({
    id: props.id,
  });

  return (
    <div
      ref={setNodeRef}
      className={cn(
        'min-h-[75px] w-full rounded-xl border flex items-center border-gray-300 transition-all bg-white p-3',
        { 'bg-purple/70 text-white': isOver },
      )}
    >
      Drag and drop the desired question here.
    </div>
  );
};

const DragDropContext = () => {
  const initialQuestions = [
    {
      id: 3956,
      content:
        'Surface tension is not strong enough to retain drops of water in rocks with large pores but it strong enough to hold on to thin films of water in rocks with small pores.',
      is_correct: false,
    },
    {
      id: 3957,
      content:
        'Water in rocks is held in place by large pores and drains away from small size pores through surface tension.',
      is_correct: false,
    },
    {
      id: 3958,
      content:
        'Small pores and large pores both interact with surface tension to determine whether a rock will hold water as heavy drops or as a thin film.',
      is_correct: false,
    },
    {
      id: 3959,
      content:
        'If the force of surface tension is too weak to hold water in place as heavy drops, the water will continue to be held firmly in place as a thin film when large pores exist.',
      is_correct: false,
    },
    {
      id: 3951,
      content:
        'Small pores and large pores both interact with surface tension to determine whether a rock will hold water as heavy drops or as a thin film.',
      is_correct: false,
    },
    {
      id: 3952,
      content:
        'If the force of surface tension is too weak to hold water in place as heavy drops, the water will continue to be held firmly in place as a thin film when large pores exist.',
      is_correct: false,
    },
  ].map((question, questionIndex) => ({
    ...question,
    content: `${String.fromCharCode(65 + questionIndex)}. ${question.content}`,
  }));

  const [questions, setQuestions] =
    useState<TTestQuestionOption[]>(initialQuestions);
  const [answers, setAnswers] = useState<TTestQuestionOption[]>([]);

  const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));

  const handleDragEnd = (event: any) => {
    setQuestions((prev) =>
      prev.filter((item) => item.id.toString() !== event.active.id),
    );
    setAnswers((prev) => [...prev, event.active.data.current]);
  };

  return (
    <DndContext
      sensors={sensors}
      collisionDetection={closestCenter}
      onDragEnd={handleDragEnd}
    >
      <div className="flex flex-col gap-4">
        <div className="gap-2 rounded-xl border border-gray-200 bg-gray-100 p-2">
          <div className="grid grid-cols-1 gap-2 lg:grid-cols-3">
            {answers.map((answer) => (
              <div
                key={answer.id}
                className="flex min-h-20 w-full items-center justify-between gap-3 rounded-xl border border-gray-300 bg-purple p-3 text-white transition-all"
              >
                <p>{answer.content}</p>
                <button
                  onClick={() => {
                    setAnswers((prev) =>
                      prev.filter((item) => item.id !== answer.id),
                    );
                    setQuestions((prev) => [...prev, answer]);
                  }}
                  className="rounded-lg bg-white p-0.5"
                >
                  <IoClose className="size-7 fill-purple" />
                </button>
              </div>
            ))}
            {answers.length < 3 &&
              [1, 2, 3]
                .splice(answers.length - 3)
                .map((emptyAnswerBox) => (
                  <DroppableAnswer key={emptyAnswerBox} id={emptyAnswerBox} />
                ))}
          </div>
        </div>
        <div
          className={cn(
            'grid grid-cols-1 lg:grid-cols-3 gap-2 rounded-xl border border-gray-200 bg-gray-100 p-2',
            {
              'pointer-events-none': answers.length === 3,
            },
          )}
        >
          {questions.map((question) => (
            <DraggableQuestion key={question.id} data={question} />
          ))}
        </div>
      </div>
    </DndContext>
  );
};
Leave a Comment