Use jQuery to allow users to like/unlike posts with dynamic updates

- Only the pages that need jQuery and the likes.js script will load it
This commit is contained in:
Xevion
2022-03-30 01:18:42 -05:00
parent bfb69621e1
commit c5c3b01dfa
7 changed files with 69 additions and 6 deletions

View File

@@ -1,7 +1,7 @@
from flask import Blueprint, redirect, render_template, url_for, request from flask import Blueprint, redirect, render_template, url_for, request, jsonify
from flask_login import current_user, login_required from flask_login import current_user, login_required
from models import User, Post, Comment from models import User, Post, Comment, PostLike, CommentLike
from forms import NewPostForm, NewCommentForm, EditProfileForm from forms import NewPostForm, NewCommentForm, EditProfileForm
from database import db from database import db
@@ -46,6 +46,27 @@ def view_post(post_id: int):
return render_template('pages/post.html', form=NewCommentForm(), post=post) return render_template('pages/post.html', form=NewCommentForm(), post=post)
@blueprint.route('/post/<post_id>/like', methods=['POST'])
@login_required
def like_post(post_id: int):
# Check that the relevant post exists
post = db.session.query(Post).get_or_404(post_id)
# Acquire the relevant PostLike in question
post_like = db.session.query(PostLike).filter_by(post=post, user=current_user).first()
if post_like is None:
post_like = PostLike(post=post, user=current_user)
db.session.add(post_like)
else:
db.session.delete(post_like)
post_like = None
db.session.commit()
# post_like is only NOT None if the user had not liked it before, but has liked it now after db.commit().
return jsonify({'liked': post_like is not None, 'status_text': post.get_like_text()})
@blueprint.route('/search') @blueprint.route('/search')
def search(): def search():
return render_template('pages/search.html') return render_template('pages/search.html')

19
static/likes.js Normal file
View File

@@ -0,0 +1,19 @@
function like(id) {
$.ajax({url: `/post/${id}/like`, method: "POST", dataType: "json"})
.done(function (data) {
let post_parent = $(`#post-${id}`)
let heart_icon = post_parent.find('.fa-heart')
let pre_liked = heart_icon.hasClass('liked')
// Toggle if the current state no longer matches the database state.
if (pre_liked !== data.liked) {
if (pre_liked)
heart_icon.removeClass('liked')
else
heart_icon.addClass('liked')
}
// Set new state of the like status text
post_parent.find('.post-like-status').html(data.status_text)
})
}

View File

@@ -257,6 +257,7 @@ nav .links li:not(:last-child)::after, footer .links li:not(:last-child)::after
top: 1em; top: 1em;
right: 1em; right: 1em;
color: #b0c9f3; color: #b0c9f3;
cursor: pointer;
} }
.post-box .fa-heart.liked { .post-box .fa-heart.liked {
color: #1b53a8; color: #1b53a8;

View File

@@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["styles.scss"],"names":[],"mappings":"AAAA;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;AAEA;EAOE;;AALA;EAEE;;;AASJ;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;;;AAKJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAKN;EACE;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAIA;EACE;EACA;;AAEA;EACE;;AAEA;EACE;;;AAMR;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAIA;EACE;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;;AAIJ;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;;;AAIJ;EACE;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;;;AAMF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;;AAKF;EACE;EACA;EACA;EACA;;;AAMR;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;;AAGF;EACE;;AAEA;EACE;EACA;;AAIJ;EACE;EACA;;AAGF;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AACA;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAIJ;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAEA;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAKN;EACE;EACA;;AAIJ;EAEE;EACA;EACA;;;AAMR;EACE;EACA;;AACA;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;;AAKN;EACE;EACA;;;AAIJ;EACE;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EAME;EACA;EACA;EAEA;EACA;EACA;;AAXA;EACE;;;AAcN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;;;AAGF;EACE","file":"styles.css"} {"version":3,"sourceRoot":"","sources":["styles.scss"],"names":[],"mappings":"AAAA;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;AAEA;EAOE;;AALA;EAEE;;;AASJ;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;;;AAKJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAKN;EACE;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAIA;EACE;EACA;;AAEA;EACE;;AAEA;EACE;;;AAMR;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAIA;EACE;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;;AAIJ;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;;;AAIJ;EACE;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;;;AAMF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;;AAKF;EACE;EACA;EACA;EACA;;;AAMR;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;;AAGF;EACE;;AAEA;EACE;EACA;;AAIJ;EACE;EACA;;AAGF;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AACA;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAIJ;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAEA;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAKN;EACE;EACA;;AAIJ;EAEE;EACA;EACA;;;AAMR;EACE;EACA;;AACA;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;;AAKN;EACE;EACA;;;AAIJ;EACE;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EAME;EACA;EACA;EAEA;EACA;EACA;;AAXA;EACE;;;AAcN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;;;AAGF;EACE","file":"styles.css"}

View File

@@ -303,6 +303,7 @@ nav, footer {
top: 1em; top: 1em;
right: 1em; right: 1em;
color: #b0c9f3; color: #b0c9f3;
cursor: pointer;
&.liked { &.liked {
color: #1b53a8; color: #1b53a8;
} }

View File

@@ -9,6 +9,25 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.5.0/js/all.min.js" <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.5.0/js/all.min.js"
integrity="sha512-YUwFoN1yaVzHxZ1cLsNYJzVt1opqtVLKgBQ+wDj+JyfvOkH66ck1fleCm8eyJG9O1HpKIf86HrgTXkWDyHy9HA==" integrity="sha512-YUwFoN1yaVzHxZ1cLsNYJzVt1opqtVLKgBQ+wDj+JyfvOkH66ck1fleCm8eyJG9O1HpKIf86HrgTXkWDyHy9HA=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script> crossorigin="anonymous" referrerpolicy="no-referrer"></script>
{% if use_jquery %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"
integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
let csrf_token = "{{ csrf_token() }}";
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrf_token);
}
}
});
</script>
{% if use_likes %}
<script src="{{ url_for('static', filename='likes.js') }}"></script>
{% endif %}
{% endif %}
<link rel="preload" href="{{ url_for('static', filename='roadrunner_header.png') }}" as="image"> <link rel="preload" href="{{ url_for('static', filename='roadrunner_header.png') }}" as="image">
{% endblock %} {% endblock %}
</head> </head>

View File

@@ -1,5 +1,7 @@
{% extends 'layouts/index.html' %} {% extends 'layouts/index.html' %}
{% from 'macros.html' import render_field %} {% from 'macros.html' import render_field %}
{% set use_jquery = true %}
{% set use_likes = true %}
{% block content %} {% block content %}
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
@@ -13,15 +15,15 @@
<hr style="margin: 1.5em 0"> <hr style="margin: 1.5em 0">
{% for post in posts %} {% for post in posts %}
<div class="post-box"> <div id="post-{{ post.id }}" class="post-box">
<i class="fas fa-heart {% if current_user.has_liked_post(post.id) %}liked{% endif %}"></i> <i class="fas fa-heart {% if current_user.has_liked_post(post.id) %}liked{% endif %}" onclick="like({{ post.id }})"></i>
{{ post.text }} {{ post.text }}
<div class="post-author no-border"> <div class="post-author no-border">
{% with comment_count = post.comments|length %} {% with comment_count = post.comments|length %}
<em>Posted by <a href="{{ url_for('main.view_user', username=post.author.username) }}">{{ post.author.name }}</a></em> <em>Posted by <a href="{{ url_for('main.view_user', username=post.author.username) }}">{{ post.author.name }}</a></em>
<span title="{{ post.date_posted }}">{{ post.get_time_ago() }} ago</span>. | <span title="{{ post.date_posted }}">{{ post.get_time_ago() }} ago</span>. |
<a href="{{ url_for('main.view_post', post_id=post.id) }}"><span> {{ comment_count }} comment{{ comment_count|pluralize }}</span></a> <a href="{{ url_for('main.view_post', post_id=post.id) }}"><span> {{ comment_count }} comment{{ comment_count|pluralize }}</span></a>
| {{ post.get_like_text()|safe }} | <span class="post-like-status">{{ post.get_like_text()|safe }}</span>
{% endwith %} {% endwith %}
</div> </div>
</div> </div>