Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

successdav's avatar

How to properly calculate and display time left on an examination app laravel

Hi, I am experiencing inconsistencies with time calculations in my Laravel online examination platform when accessing it from different machines. Currently it is access via locacl host. php artisan serve --host 192.168.0.101 --port 80. the time left will either be higher than the speculated time for the quiz or less than while in a few other laptops it is correct.

I keep track of when the users starts the exams in my db

 public function up(): void
    {
        Schema::create('exam_histories', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->foreignId('assessment_id')->constrained()->onDelete('cascade');
            $table->foreignId('quiz_id')->constrained()->onDelete('cascade');
            $table->date('exam_date');
            $table->timestamp('start_time');
            $table->timestamp('ends_at')->nullable();

            $table->timestamps();
        });
    }

Before the exams starts I return the page along with the time start stored in the database

     return Inertia::render('User/TakeAssessment/Live-Exam', [
            'quiz'          => new QuizResource($quiz),
            'questions'     => $quiz->questions->shuffle(),
            'start_time'    => $session->start_time,
            'user_responses'=> $user_responses
        ]);

then on the vue page the start time is received and passed to the timer component

<script>

export default {
    layout: Layout,
    components: {Modal, Timer, Question, Questions},
    props: ['questions','quiz','start_time','user_responses'],

The Timer Component

<template>
  <div>
    <span v-if="!hasEnded">{{ timeLeft }}</span>
    <span v-else>Time's Up!</span>
  </div>
</template>

<script>
export default {
  props: {
    duration: {
      type: Number,
      required: true,
    },
    startTime: {
      type: Date,
      required: true,
    },
  },
  data() {
    return {
      intervalId: null,
      currentTime: new Date(),
    };
  },
  computed: {
    timeLeft() {
      const targetTime = new Date(this.startTime);
      targetTime.setMinutes(targetTime.getMinutes() + this.duration);
      const difference = targetTime.getTime() - this.currentTime.getTime();

      if (difference <= 0) {
        return null;
      }

      const seconds = Math.floor((difference / 1000) % 60);
      const minutes = Math.floor((difference / (1000 * 60)) % 60);
      const hours = Math.floor((difference / (1000 * 60 * 60)) % 24);

      return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
    },
    hasEnded() {
      return this.timeLeft === null;
    },
  },
  mounted() {
    this.intervalId = setInterval(this.updateTime, 1000);
  },
  beforeDestroy() {
    clearInterval(this.intervalId);
  },
  methods: {
    updateTime() {
      this.currentTime = new Date();
      if (this.hasEnded) {
        this.$emit('timeUp'); // Emit event when time is up
      }
      this.$forceUpdate();
    },
  },
};
</script>

<style scoped>
/* Add your styling here */
</style>
0 likes
1 reply
martinbean's avatar
Level 80

@successdav You should be storing all dates and times as UTC in your database. Calling Date.now() will then return a Unix timestamp.

I find your model confusing as you have an ends_at timestamp, but then use the start_time timestamp for comparison. Surely you want a timer counting down to the end time, not the start?

Regardless, your controllers should reject any requests made after the end time, so that users can’t keep manually submitting requests after the end, or mess with the timer on the client side.

abort_if(Date::now()->gt($exam->end_date), 'Exam has ended.');

Please or to participate in this conversation.