Task Management

import React from "react";

const DEFAULT_TASK = {
  id: crypto.randomUUID(),
  displayName: "Primary Test Label",
  status: "Active"
}

const TaskList = () => {
  const [taskList, setTaskList] = React.useState([DEFAULT_TASK]);

  const upsertTask = (updatedTask) => {
    const itemIndex = taskList.findIndex((x) => x.id === updatedTask.id);
    if (itemIndex >= 0) {
      const listClone = [...taskList];
      listClone[itemIndex] = updatedTask;
      setTaskList(listClone);
    } else {
      setTaskList([...taskList, updatedTask]);
    }
  };
	
	const onCheckChanged = (task) => {
    upsertTask({
      ...task,
      status: task.status === "Completed" ? "Active" : "Completed"
    });
  };
	
  const renderCheckBox = (task) => {
    return (
      <input
        type="checkbox"
        checked={task.status === "Completed" ? "checked" : ""}
        onChange={() => onCheckChanged(task)}
      />
    );
  };

	const onTaskLabelClick = (task) => {
      upsertTask ({
        ...task,
        isFocused: true
      });
    };
    
  const onTaskInputChanged = (task, value) => {
    upsertTask({
      ...task,
      displayName: value
    });
  };

  const onTaskInputBlured = (task) => {
    upsertTask({
      ...task,
      isFocused: false
    });
  };

  const renderDisplayName = (task) => {
    if (!task.isFocused) return <span onClick={() => onTaskLabelClick(task)}>{task.displayName}</span>;
    
		return (
      <>
        <input
          type="text"
          onChange={(e) => onTaskInputChanged(task, e.target.value)}
          onBlur={() => onTaskInputBlured(task)}
          value={task.displayName}
        />
      </>
    );
  };

  const renderTaskAsRow = (task) => (
    <tr key={task.id}>
      <td>{renderCheckBox(task)}</td>
      <td>{renderDisplayName(task)}</td>
    </tr>
  );

  const renderTaskList = () => {
    if (taskList.length === 0) return <></>;

    return (
      <table style={{ width: "100%" }}>
        <thead>
          <th style={{ width: "20px" }}>Status</th>
          <th>Name</th>
        </thead>
        <tbody>{taskList.map(renderTaskAsRow)}</tbody>
      </table>
    );
  };

  const onAddButtonClicked = () => {
    upsertTask({
      id: crypto.randomUUID(),
      displayName: "New label",
      status: "Active",
      isFocused: true,
    });
  };

  const renderActions = () => (
    <div style={{ marginBottom: "10px" }}>
      <button onClick={onAddButtonClicked}>Add task</button>
    </div>
  );

  return (
    <>
      {renderActions()}
      {renderTaskList()}
    </>
  );
};

export default function App() {
  return (
    <div className="App">
      <h1>Yet another task manager!</h1>
      <TaskList />
    </div>
  );
}

Quiz Management

import React from "react";
import styles from "./page.module.css";

type Question = {
  question: string;
  answers: string[];
  correct_index: number;
};

enum QuizState {
  NoStarted,
  Initiated,
  TimeOut,
  Completed,
}

interface INotStartedBannerProps {
  onStartQuiz: () => void;
}

const NotStartedBanner = (props: INotStartedBannerProps) => (
  <>
    <h1 className={styles.h1}>Welcome to your ultimate quiz!</h1>
    <button type="button" className={styles.button} onClick={props.onStartQuiz}>
      Start the quiz now!
    </button>
  </>
);

const TimeoutBanner = () => (
  <>
    <h1 className={styles.h1}>You ran out of time!</h1>
  </>
);

interface IQuizCompletedBannerProps {
  questions: Question[];
  answers: number[];
}

const QuizCompleted = (props: IQuizCompletedBannerProps) => (
  <>
    <h1 className={styles.h1}>You completed the quiz!</h1>
    <div style={{ textAlign: "left" }}>
      {props.questions.map((q, index) => {
        return (
          <div key={`question-result-${index}`} style={{ marginBottom: "15px" }}>
            <div>{`Question ${index + 1}: ${q.question}`}</div>
            <div>{`Your answer: ${q.answers[props.answers[index]]} ${q.correct_index === props.answers[index] ? "Correct" : "Incorrect"}`}</div>
          </div>
        );
      })}
    </div>
  </>
);

const fetchQuestions: () => Promise<Question[]> = async () => {
  try {
    const response = await fetch("<http://interview.workwhilejobs.com/quiz/questions>");
    return (await response.json()) as Question[];
  } catch (e: any) {
    console.log(e);
    return [];
  }
};

interface IQuestionProps {
  currentIndex: number;
  totalQuestions: number;
  question: Question;
  currentSelectedAnswer?: number;
  onQuestionAnswered: (questionIndex: number, answerIndex: number | undefined, isNext: boolean) => void;
  timerValue: number;
}

const Question = (props: IQuestionProps) => {
  const { currentIndex, totalQuestions, question, currentSelectedAnswer, onQuestionAnswered: onUpdateAnswer, timerValue } = props;
  const [selection, setSelection] = React.useState<number | undefined>(currentSelectedAnswer);

  const onAnswerChanged = (event: any) => {
    const selectedIndex = event.target.value;
    setSelection(parseInt(selectedIndex));
  };
  const onSubmit = (isNext: boolean) => onUpdateAnswer(currentIndex, selection, isNext);

  return (
    <>
      <div style={{ marginBottom: "10px", textAlign: "center" }}>
        <div>Timer: {timerValue}</div>
        <div>
          Question {currentIndex + 1}/{totalQuestions}
        </div>
      </div>
      <div style={{ marginBottom: "10px" }}>{question.question}</div>
      <div onChange={onAnswerChanged}>
        {question.answers.map((answer, index) => (
          <div key={`question-${currentIndex}-${index}`}>
            <label>
              <input type="radio" value={index} name="question" checked={selection === index} />
              <span style={{ marginLeft: "5px" }}>{answer}</span>
            </label>
          </div>
        ))}
      </div>

      <div style={{ marginTop: "10px" }}>
        <button style={{ marginRight: "10px" }} onClick={() => onSubmit(false)}>
          Back
        </button>
        <button onClick={() => onSubmit(true)}>{currentIndex === totalQuestions - 1 ? "Finish" : "Next"}</button>
      </div>
    </>
  );
};

const Page = () => {
  const [quizState, setQuizState] = React.useState<QuizState>(QuizState.NoStarted);

  const quizTimeLimitInSeconds = 90;
  // Session timer: initialized when user starts the quiz.
  const [timerValue, setTimerValue] = React.useState<number>(quizTimeLimitInSeconds);

  // List of available questions fetched from the API.
  const [questions, setQuestions] = React.useState<Question[]>([]);
  // One question per page. Questions are identified based on their position on the response. (No id available)
  const [currentQuestionIndex, setCurrentQuestionIndex] = React.useState<number>(0);
  // User answers to each question.
  const [answers, setAnswers] = React.useState<number[]>([]);

  const decrementQuizTime = () => {
    const newTime = timerValue - 1;
    setTimerValue(newTime);
    newTime === 0 && setQuizState(QuizState.TimeOut);
  };

  React.useEffect(() => {
    const fetchResults = async () => {
      const results = await fetchQuestions();
      setQuestions(results);
    };
    fetchResults();
  }, []);

  React.useEffect(() => {
    quizState === QuizState.Initiated && timerValue > 0 && setTimeout(decrementQuizTime, 1000);
  }, [quizState, timerValue, decrementQuizTime]);

  const startAQuiz = () => {
    setQuizState(QuizState.Initiated);
  };

  if (quizState === QuizState.NoStarted) return <NotStartedBanner onStartQuiz={startAQuiz} />;
  if (quizState === QuizState.TimeOut) return <TimeoutBanner />;
  if (quizState === QuizState.Completed) return <QuizCompleted questions={questions} answers={answers} />;

  // quizState === QuizState.Initiated:
  const currentQuestion = questions.at(currentQuestionIndex)!;
  const onQuestionAnswered = (questionIndex: number, answerIndex: number | undefined, isNext: boolean) => {
    if (answerIndex) {
      const updateAnswers = [...answers];
      updateAnswers[questionIndex] = answerIndex;
      setAnswers(updateAnswers);
    }

    if (isNext && currentQuestionIndex === questions.length - 1) {
      setQuizState(QuizState.Completed); // This will stop the timer as well.
    } else {
      const newIndex = isNext ? currentQuestionIndex + 1 : currentQuestionIndex - 1;
      setCurrentQuestionIndex(newIndex);
    }
  };

  return (
    <Question
      key={currentQuestionIndex}
      currentIndex={currentQuestionIndex}
      totalQuestions={questions.length}
      question={currentQuestion}
      currentSelectedAnswer={answers[currentQuestionIndex]}
      timerValue={timerValue}
      onQuestionAnswered={onQuestionAnswered}
    />
  );
};

export default Page;