Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 61 additions & 133 deletions authenticate/templates/account.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@

{% block content %}
<div class="container py-4 anim-l-1">
<h2 class="mb-4 all-l-ani-1">@{{ profileUser.username }}'s Profile</h2>
<h2 class="mb-4 all-l-ani-1">User's Profile</h2>

<div class="card mb-4 all-l-ani-1">
<div class="card mb-4 all-l-ani-1" style="overflow: hidden;">
<div class="card-body all-l-ani-1">

<p class="card-text mb-1 all-l-ani-1"><strong>Email:</strong>
<p class="card-text mb-1 all-l-ani-1"><strong>Username:</strong>
{{ profileUser.username }}
</p>

<p class="card-text mb-1 all-l-ani-1 mt-3"><strong>Email:</strong>
{% if profileUser.email %}
{{ profileUser.email }}
{% else %}
<span class="text-muted all-l-ani-1">Not set</span>
{% endif %}
</p>

<div class="seperator-2"></div>

<p class="card-text mb-1 all-l-ani-1"><strong>Joined on:</strong> {{ profileUser.date_joined|date:"F j, Y" }}</p>

{% if user == profileUser %}
Expand All @@ -27,146 +33,68 @@ <h2 class="mb-4 all-l-ani-1">@{{ profileUser.username }}'s Profile</h2>
</div>
</div>

<h4 class="mb-3 all-l-ani-1">{{ profileUser.username }}'s Posts</h4>
{% if posts %}
<div class="list-group all-l-ani-1" id="posts-container">
{% for post in posts %}
<div class="list-group-item position-relative hover-anim-2 all-l-ani-1">
<!--------------------------------------------------------------------------------------------------->
<!-- Delete Button -->

{% if request.user == post.author or request.user.is_staff %}
<form method="post" action="/posts/{{ post.id }}/delete/"
class="position-absolute top-0 end-0 m-2 all-l-ani-1"
onsubmit="return confirm('Are you sure you want to delete this post?');">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.path }}">
<button type="submit" class="all-l-ani-1 btn btn-sm btn-outline-danger" title="Delete">
<i class="bi bi-trash"></i>
</button>
</form>
{% endif %}
<!--------------------------------------------------------------------------------------------------->
<h5 class="mb-1 all-l-ani-1">{{ post.title }}</h5>

<small class="text-muted all-l-ani-1">Posted by @{{ post.author }} on {{ post.date|date:"F j, Y, g:i a" }}</small>
<!--------------------------------------------------------------------------------------------------->
<div class="post-body post-body-clamp all-l-ani-1">
{{ post.body }}
</div>
<a class="see-more-text link-secondary text-decoration-underline all-l-ani-1" href="#">See more...</a>
<!--------------------------------------------------------------------------------------------------->
<!--vote options-->
<div class="d-flex align-items-center mt-4 all-l-ani-1" data-post-id="{{ post.id }}">
<button
class="btn all-l-ani-1 btn-sm {% if post.userVote == 'up' %}btn-success{% else %}btn-outline-success{% endif %} me-2 upvote-btn">
👍 <span class="upvote-count all-l-ani-1">{{ post.get_upvotes }}</span>
</button>

<button
class="btn all-l-ani-1 btn-sm {% if post.userVote == 'down' %}btn-danger{% else %}btn-outline-danger{% endif %} downvote-btn">
👎 <span class="downvote-count all-l-ani-1">{{ post.get_downvotes }}</span>
</button>
</div>

</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info hover-anim-2 all-l-ani-1">
{% if user == profileUser %}
You haven't posted anything yet.
{% else %}
{{ profileUser.username }} hasn't posted anything yet.
{% endif %}
<h4 class="mb-3 all-l-ani-1">User's Details</h4>
<div class="card mb-4 all-l-ani-1" style="overflow: auto;">
<div class="card-body card-usr-deltail all-l-ani-1">
<div class="card-p-p-1">
<h3 class="h2-p-p-1-v h2-post no-wrap-p-1">Posts</h3>
<span class="no-wrap-p-1 data-count" data-counts="{{ profileUser.post_count }}">0</span>
</div>
<div class="card-p-p-1">
<h3 class="h2-p-p-1-v h2-vote no-wrap-p-1">Votes</h3>
<span class="no-wrap-p-1 data-count" data-counts="{{ profileUser.vote_count }}">0</span>
</div>
<div class="card-p-p-1">
<h3 class="h2-p-p-1-v h2-bookmark no-wrap-p-1">Bookmarks</h3>
<span class="no-wrap-p-1 data-count" data-counts="{{ profileUser.bookmark_count }}">0</span>
</div>
<div class="card-p-p-1">
<h3 class="h2-p-p-1-v h2-comment no-wrap-p-1">Comments</h3>
<span class="no-wrap-p-1 data-count" data-counts="328789223233">0</span>
</div>
</div>
{% endif %}
</div>

</div>
{% endblock %}

{% block extra_scripts %}
<script>
//Voting logics
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.upvote-btn, .downvote-btn').forEach(button => {
button.addEventListener('click', function () {
const postElement = this.closest('[data-post-id]');
const postId = postElement.getAttribute('data-post-id');
const voteType = this.classList.contains('upvote-btn') ? 'up' : 'down';

fetch('/posts/vote/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({ postId, voteType })
})
.then(res => res.json())
.then(data => {
if (data.up !== undefined && data.down !== undefined) {
const formatter = new Intl.NumberFormat('en', {
notation: 'compact',
compactDisplay: 'short'
});

postElement.querySelector('.upvote-count').textContent = formatter.format(data.up);
postElement.querySelector('.downvote-count').textContent = formatter.format(data.down);

const upBtn = postElement.querySelector('.upvote-btn');
const downBtn = postElement.querySelector('.downvote-btn');

upBtn.classList.remove('btn-success', 'btn-outline-success');
downBtn.classList.remove('btn-danger', 'btn-outline-danger');

if (data.userVote === 'up') {
upBtn.classList.add('btn-success');
downBtn.classList.add('btn-outline-danger');
} else if (data.userVote === 'down') {
upBtn.classList.add('btn-outline-success');
downBtn.classList.add('btn-danger');
} else {
upBtn.classList.add('btn-outline-success');
downBtn.classList.add('btn-outline-danger');
}
document.addEventListener("DOMContentLoaded", function () {
const counters = document.querySelectorAll(".data-count");
const speed = 80n; // tốc độ (sử dụng BigInt)

const animateCount = (counter) => {
let start = 0n; // BigInt
const target = BigInt(counter.getAttribute("data-counts"));

const updateCount = () => {
const inc = target / speed + 1n; // bước nhảy, luôn >= 1
if (start < target) {
start = start + inc;
if (start > target) start = target;
counter.innerText = shortenNumber(start);
requestAnimationFrame(updateCount);
} else {
console.error(data.error);
counter.innerText = shortenNumber(target);
}
})
.catch(err => console.error('Request failed:', err));
});
});
});

//See more / See less logic
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.post-body').forEach(post => {
const seeMore = post.nextElementSibling;

// Create a clone to measure height
const clone = post.cloneNode(true);
clone.style.position = 'absolute';
clone.style.visibility = 'hidden';
clone.style.height = 'auto';
clone.style.maxHeight = 'none';
clone.style.webkitLineClamp = 'unset';
clone.classList.remove('post-body-clamp');
document.body.appendChild(clone);

if (clone.offsetHeight <= post.offsetHeight) {
seeMore.style.display = 'none';
}

document.body.removeChild(clone);
});
};
updateCount();
};

// Dùng IntersectionObserver
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
animateCount(entry.target);
obs.unobserve(entry.target); // chỉ chạy 1 lần
}
});
}, { threshold: 0.3 });

document.querySelectorAll('.see-more-text').forEach(span => {
span.addEventListener('click', function () {
const post = this.previousElementSibling;
post.classList.toggle('expanded');
this.textContent = post.classList.contains('expanded') ? 'See less' : 'See more...';
counters.forEach(counter => {
observer.observe(counter);
});
});
});
</script>
{% endblock %}
30 changes: 26 additions & 4 deletions authenticate/templates/accountsList.html
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,28 @@ <h1 class="h3 all-l-ani-1">Users</h1>
<div class="list-group all-l-ani-1">
{% for user in page_obj %}
<div class="hover-anim-1 list-group-item d-flex justify-content-between align-items-start position-relative user-clickable" data-href="{{ user.id }}" style="cursor: pointer;">

<div class="user-card-1">
<h5 class="gluser1 all-l-ani-1">@{{ user.username }}</h5>
<small class="text-muted all-l-ani-1">
Joined on {{ user.date_joined|date:"M d, Y" }}
{% if user.is_staff %}- <span class="text-danger">Administrator</span>{% endif %}
<div class="d-flex" style="flex-direction: column;">
<p>
Joined on {{ user.date_joined|date:"M d, Y" }}
{% if user.is_staff %}- <span class="text-danger">Administrator</span>{% endif %}
</p>
<a class="btn ab-1 all-l-ani-1 btn-sm btn-outline-primary post-btn" href="/posts/?user={{ user.id }}" style="width: 7rem !important;">
<div class="dv-svg-p-p-1">
<svg class="post-svg" width="17" height="17" viewBox="0 0 24 24"
fill="none"
stroke="currentColor" stroke-width="2"
aria-hidden="true" role="img" aria-label="post">
<path d="M6 2h7l5 5v13a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/>
<line x1="8" y1="9" x2="16" y2="9"/>
<line x1="8" y1="12" x2="16" y2="12"/>
</svg>
</div>
<span class="post-count rep-l-1 all-l-ani-1" style="display:flex;padding-left: 1.1rem;" data-postscnt="{{ user.post_count }}"></span>
</a>
</div>
</small>
{% if user.profile.bio %}
<div class="mt-2 text-muted small all-l-ani-1">{{ user.profile.bio }}</div>
Expand Down Expand Up @@ -224,10 +240,16 @@ <h5 class="gluser1 all-l-ani-1">@{{ user.username }}</h5>
{% endif %}
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const postCnt = document.querySelectorAll(".post-count");
postCnt.forEach((e) => {
e.textContent = shortenNumber(`${e.dataset.postscnt}`);
});
});
// Make user blocks clickable
document.querySelectorAll(".user-clickable").forEach(item => {
item.addEventListener("click", e => {
if (!e.target.closest("form")) {
if (!(e.target.closest("form") || e.target.closest("a"))) {
window.location = item.dataset.href;
}
});
Expand Down
47 changes: 30 additions & 17 deletions authenticate/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from post.models import Post, Vote
from django.db.models import OuterRef, Subquery, CharField, Value
from post.models import Post, Vote, Bookmark
from django.db.models import OuterRef, Subquery, CharField, Value, Count, Q
from django.db.models.functions import Coalesce
from django.core.paginator import Paginator

Expand All @@ -19,25 +19,22 @@ def signup(request):


def viewProfile(request, profileId):
profileUser = get_object_or_404(User, id=profileId)

# Subquery to get the current user's vote on each post
user_vote_subquery = Vote.objects.filter(
post=OuterRef('pk'),
user=request.user
).values('voteType')[:1]

# Annotate each post with the user's vote
posts = Post.objects.filter(author=profileUser).order_by('-date').annotate(
userVote=Coalesce(
Subquery(user_vote_subquery, output_field=CharField()),
Value('none')
)
profileUser = get_object_or_404(
User.objects.annotate(
post_count=Count("post", distinct=True), # số post của user
vote_count=Count("post__votes", distinct=True), # tổng vote (up + down)
bookmark_count=Count("post__bookmarks", distinct=True) # tổng bookmark
),
id=profileId
)

# profileUser.email = "223791873128739128379128371298379223791873128739128379128371222379187312873912837912837129837922379187312873912837912837129837922379187312873912837912837129837998379223791873128739128379128371298379"

if (profileUser.email is not None) and (len(profileUser.email) > 110):
profileUser.email = profileUser.email[:(110 - 3)] + "..."

return render(request, "account.html", {
"profileUser": profileUser,
"posts": posts
})


Expand All @@ -51,14 +48,30 @@ def viewProfile(request, profileId):
def accountsList(request):
if not request.user.is_authenticated:
return redirect("/")

max_len_of_username01 = 130

users = User.objects.all().order_by('-date_joined')

# Annotate thêm dữ liệu
users = users.annotate(
post_count=Count("post", distinct=True), # số post của user
vote_count=Count("post__votes", distinct=True), # tổng vote (up + down)
like_count=Count("post__votes", filter=Q(post__votes__voteType="up"), distinct=True), # tổng like
dislike_count=Count("post__votes", filter=Q(post__votes__voteType="down"), distinct=True), # tổng dislike
bookmark_count=Count("post__bookmarks", distinct=True) # tổng bookmark
)

# Phân trang (10 user/trang)
paginator = Paginator(users, 10)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)

for user in page_obj:
if len(user.username) > max_len_of_username01:
user.username = user.username[:(max_len_of_username01 - 3)] + "..."


return render(request, "accountsList.html", {
"users": page_obj.object_list,
"page_obj": page_obj,
Expand Down
Binary file removed db.sqlite3
Binary file not shown.
Loading