Why does my Flask website keep refreshing?

I am trying to make a social media website, (similar to TikTok) but the watch page keeps refreshing.

My JS sends JSON like this to the backend, which keeps track of likes, dislikes, views, etc. in a SQLite database:

{
  "action": {action e.g. like, dislike, view}
  "id": {video_id e.g. 1)
}

Then, the backend processes it with this function:

@app.route('/trackStatistics', methods=['POST'])
def track_statistics():
    try:
        # Get the JSON data from the request
        data = request.get_json()

        # Validate the action and id
        if 'action' not in data or 'id' not in data:
            return jsonify({'error': 'Invalid request'}), 400

        action = data['action']
        video_id = data['id']

        data = get_video(video_id)

        print(data)

        if action == "like":
            data["likes"] = int(data["likes"]) + 1
        elif action == "dislike":
            data["dislikes"] = int(data["dislikes"]) + 1
        elif action == "unlike":
            data["likes"] = int(data["likes"]) - 1
        elif action == "undislike":
            data["dislikes"] = int(data["dislikes"]) - 1
        elif action == "view":
            data["views"] = int(data["views"]) + 1
        
        update_video(video_id, data["publisher"], data["title"], data["likes"], data["dislikes"], data["views"], data["comments"], False)

        print(data)

        # Return the updated data
        return jsonify(data), 200

    except Exception as e:
        print(str(e))
        return jsonify({'error': str(e)}), 500

Here’s the file with my update_video, get_video, and create_video functions defined:

import sqlite3
import json
from datetime import datetime

def get_video(video_id):
    conn = sqlite3.connect('../databases/videos.db')
    c = conn.cursor()
    c.execute("SELECT * FROM videos WHERE id = ?", (video_id,))
    video = c.fetchone()
    comments = json.loads(video[6]) if video[6] else []
    video = list(video)
    video[6] = comments
    conn.close()
    return {
        "id": video[0],
        "publisher": video[1],
        "title": video[2],
        "likes": video[3],
        "dislikes": video[4],
        "views": video[5],
        "comments": video[6],
        "created_at": video[7],
        "updated_at": video[8]
    }

def create_video(publisher, title, likes, dislikes, views, comments):
    conn = sqlite3.connect('../databases/videos.db')
    c = conn.cursor()
    now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    comments_json = json.dumps(comments)
    c.execute("INSERT INTO videos (publisher, title, likes, dislikes, views, comments, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", 
              (publisher, title, likes, dislikes, views, comments_json, now, now))
    conn.commit()
    conn.close()
    return c.lastrowid

def update_video(video_id, publisher, title, likes, dislikes, views, comments, update_time):
    conn = sqlite3.connect('../databases/videos.db')
    c = conn.cursor()

    # Get the current updated_at time
    c.execute("SELECT updated_at FROM videos WHERE id = ?", (video_id,))
    last_updated = c.fetchone()[0]

    if update_time:
        now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    else:
        now = last_updated

    comments_json = json.dumps(comments)
    c.execute("UPDATE videos SET publisher = ?, title = ?, likes = ?, dislikes = ?, views = ?, comments = ?, updated_at = ? WHERE id = ?",
              (publisher, title, likes, dislikes, views, comments_json, now, video_id))
    conn.commit()
    conn.close()

I tried to keep track of the video stats and the server seems to work, it prints the correct data! But, after giving code 200 success, it refreshes the page.

Here’s the JS I’m using to interact with the backend:

let liked = false;
let disliked = false;
let followed = false;
let commentsOpen = false;
const url = window.location.search;
const urlParams = new URLSearchParams(url);
const id = urlParams.get('c');
const trackStatisticsRoute = 'http://localhost:5005/trackStatistics';

$(document).ready(() => {
  if (id) {
    document.title = "ByteClips - " + id;
  }

  const vidFile = `../videos/${id}/video.mp4`;

  console.log(vidFile);
  $('#vidSrc').attr('src', vidFile);
  $('.video')[0].load();

    updateRating('view', id);

  $('.video-buttons div button').hover(
    () => {
      $(this).find('i').css('opacity', 0);
      $(this).find('.counter').css('opacity', 1);
    },
    () => {
      $(this).find('i').css('opacity', 1);
      $(this).find('.counter').css('opacity', 0);
    }
  );
});

function updateRating(action, id) {
  const requestData = {
    action,
    id
  };

  $.ajax({
    type: 'POST',
    url: trackStatisticsRoute,
    data: JSON.stringify(requestData),
    contentType: 'application/json',
    success: data => {
      updateUI(data);
    },
    error: (jqXHR, textStatus, errorThrown) => {
      console.log('AJAX request error:', textStatus, errorThrown);
      console.log('Response:', jqXHR.responseText);
    }
  });
}

function updateUI(data) {
  const newData = typeof data === 'string' ? JSON.parse(data) : data;
  $('#like .counter').text(newData.likes);
  $('#dislike .counter').text(newData.dislikes);
  $('#share .counter').text(newData.views);
  $('#comment .counter').text(newData.comments.length);
}

$('#like').css('background', liked ? '#fff' : '#26daa5');
$('#like').css('color', liked ? '#26daa5' : '#fff');
$('#dislike').css('background', disliked ? '#fff' : '#26daa5');
$('#dislike').css('color', disliked ? '#26daa5' : '#fff');
$('#follow').css('background', followed ? '#fff' : '#26daa5');
$('#follow').css('color', followed ? '#26daa5' : '#fff');
$('#follow i').attr('class', followed ? 'fa-solid fa-user-minus' : 'fa-solid fa-user-plus');

$('#like').on('click', () => {
  if (liked) {
    liked = false;
    updateRating('unlike', id);
  } else {
    if (disliked) {
      updateRating('undislike', id);
    }
    liked = true;
    disliked = false;
    updateRating('like', id);
  }
  updateLikeDislike();
});

$('#dislike').on('click', () => {
  if (disliked) {
    disliked = false;
    updateRating('undislike', id);
  } else {
    if (liked) {
      updateRating('unlike', id);
    }
    disliked = true;
    liked = false;
    updateRating('dislike', id);
  }
  updateLikeDislike();
});

$('#follow').on('click', () => {
  followed = !followed;
  $('#follow i').attr('class', followed ? 'fa-solid fa-user-minus' : 'fa-solid fa-user-plus');
  $('#follow').css('background', followed ? '#fff' : '#26daa5');
  $('#follow').css('color', followed ? '#26daa5' : '#fff');
});

$('#comment').on('click', () => {
  console.log('comments opened');
  commentsOpen = !commentsOpen;
  $('#comment').css('background', commentsOpen ? '#fff' : '#26daa5');
  $('#comment').css('color', commentsOpen ? '#26daa5' : '#fff');
});

function updateLikeDislike() {
  $('#like').css('background', liked ? '#fff' : '#26daa5');
  $('#like').css('color', liked ? '#26daa5' : '#fff');
  $('#dislike').css('background', disliked ? '#fff' : '#26daa5');
  $('#dislike').css('color', disliked ? '#26daa5' : '#fff');
}