More fixes
This commit is contained in:
parent
96cf1fe5ad
commit
d42f9b5339
5 changed files with 466 additions and 15 deletions
|
|
@ -72,22 +72,31 @@ public class TimelineController {
|
||||||
/**
|
/**
|
||||||
* Get the public timeline.
|
* Get the public timeline.
|
||||||
* Shows all public activities from all users.
|
* Shows all public activities from all users.
|
||||||
|
* Optionally authenticated - if user is logged in, will show which activities they've liked.
|
||||||
*
|
*
|
||||||
* GET /api/timeline/public?page=0&size=20
|
* GET /api/timeline/public?page=0&size=20
|
||||||
*
|
*
|
||||||
|
* @param userDetails the authenticated user details (optional)
|
||||||
* @param page page number (default: 0)
|
* @param page page number (default: 0)
|
||||||
* @param size page size (default: 20)
|
* @param size page size (default: 20)
|
||||||
* @return page of timeline activities
|
* @return page of timeline activities
|
||||||
*/
|
*/
|
||||||
@GetMapping("/public")
|
@GetMapping("/public")
|
||||||
public ResponseEntity<Page<TimelineActivityDTO>> getPublicTimeline(
|
public ResponseEntity<Page<TimelineActivityDTO>> getPublicTimeline(
|
||||||
|
@AuthenticationPrincipal(errorOnInvalidType = false) UserDetails userDetails,
|
||||||
@RequestParam(defaultValue = "0") int page,
|
@RequestParam(defaultValue = "0") int page,
|
||||||
@RequestParam(defaultValue = "20") int size
|
@RequestParam(defaultValue = "20") int size
|
||||||
) {
|
) {
|
||||||
log.debug("Public timeline request");
|
UUID userId = null;
|
||||||
|
if (userDetails != null) {
|
||||||
|
userId = getUserId(userDetails);
|
||||||
|
log.debug("Public timeline request from authenticated user: {}", userId);
|
||||||
|
} else {
|
||||||
|
log.debug("Public timeline request (unauthenticated)");
|
||||||
|
}
|
||||||
|
|
||||||
Pageable pageable = PageRequest.of(page, size);
|
Pageable pageable = PageRequest.of(page, size);
|
||||||
Page<TimelineActivityDTO> timeline = timelineService.getPublicTimeline(pageable);
|
Page<TimelineActivityDTO> timeline = timelineService.getPublicTimeline(userId, pageable);
|
||||||
|
|
||||||
return ResponseEntity.ok(timeline);
|
return ResponseEntity.ok(timeline);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,11 @@ public class TimelineActivityDTO {
|
||||||
private String avatarUrl;
|
private String avatarUrl;
|
||||||
private boolean isLocal;
|
private boolean isLocal;
|
||||||
|
|
||||||
|
// Social interaction counts
|
||||||
|
private Long likesCount;
|
||||||
|
private Long commentsCount;
|
||||||
|
private Boolean likedByCurrentUser;
|
||||||
|
|
||||||
// Metrics summary
|
// Metrics summary
|
||||||
private ActivityMetricsSummary metrics;
|
private ActivityMetricsSummary metrics;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ public class TimelineService {
|
||||||
private final ActivityRepository activityRepository;
|
private final ActivityRepository activityRepository;
|
||||||
private final FollowRepository followRepository;
|
private final FollowRepository followRepository;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private final org.operaton.fitpub.repository.LikeRepository likeRepository;
|
||||||
|
private final org.operaton.fitpub.repository.CommentRepository commentRepository;
|
||||||
|
|
||||||
@Value("${fitpub.base-url}")
|
@Value("${fitpub.base-url}")
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
|
|
@ -74,12 +76,19 @@ public class TimelineService {
|
||||||
if (activityUser == null) {
|
if (activityUser == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return TimelineActivityDTO.fromActivity(
|
TimelineActivityDTO dto = TimelineActivityDTO.fromActivity(
|
||||||
activity,
|
activity,
|
||||||
activityUser.getUsername(),
|
activityUser.getUsername(),
|
||||||
activityUser.getDisplayName() != null ? activityUser.getDisplayName() : activityUser.getUsername(),
|
activityUser.getDisplayName() != null ? activityUser.getDisplayName() : activityUser.getUsername(),
|
||||||
activityUser.getAvatarUrl()
|
activityUser.getAvatarUrl()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add social interaction counts
|
||||||
|
dto.setLikesCount(likeRepository.countByActivityId(activity.getId()));
|
||||||
|
dto.setCommentsCount(commentRepository.countByActivityIdAndNotDeleted(activity.getId()));
|
||||||
|
dto.setLikedByCurrentUser(likeRepository.existsByActivityIdAndUserId(activity.getId(), userId));
|
||||||
|
|
||||||
|
return dto;
|
||||||
})
|
})
|
||||||
.filter(dto -> dto != null)
|
.filter(dto -> dto != null)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
@ -91,11 +100,12 @@ public class TimelineService {
|
||||||
* Get the public timeline.
|
* Get the public timeline.
|
||||||
* Shows all public activities from all users.
|
* Shows all public activities from all users.
|
||||||
*
|
*
|
||||||
|
* @param userId optional user ID for checking liked status (null for unauthenticated)
|
||||||
* @param pageable pagination parameters
|
* @param pageable pagination parameters
|
||||||
* @return page of timeline activities
|
* @return page of timeline activities
|
||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Page<TimelineActivityDTO> getPublicTimeline(Pageable pageable) {
|
public Page<TimelineActivityDTO> getPublicTimeline(UUID userId, Pageable pageable) {
|
||||||
log.debug("Fetching public timeline");
|
log.debug("Fetching public timeline");
|
||||||
|
|
||||||
// Fetch all public activities
|
// Fetch all public activities
|
||||||
|
|
@ -111,12 +121,25 @@ public class TimelineService {
|
||||||
if (activityUser == null) {
|
if (activityUser == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return TimelineActivityDTO.fromActivity(
|
TimelineActivityDTO dto = TimelineActivityDTO.fromActivity(
|
||||||
activity,
|
activity,
|
||||||
activityUser.getUsername(),
|
activityUser.getUsername(),
|
||||||
activityUser.getDisplayName() != null ? activityUser.getDisplayName() : activityUser.getUsername(),
|
activityUser.getDisplayName() != null ? activityUser.getDisplayName() : activityUser.getUsername(),
|
||||||
activityUser.getAvatarUrl()
|
activityUser.getAvatarUrl()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add social interaction counts
|
||||||
|
dto.setLikesCount(likeRepository.countByActivityId(activity.getId()));
|
||||||
|
dto.setCommentsCount(commentRepository.countByActivityIdAndNotDeleted(activity.getId()));
|
||||||
|
|
||||||
|
// Check if current user liked this activity (if authenticated)
|
||||||
|
if (userId != null) {
|
||||||
|
dto.setLikedByCurrentUser(likeRepository.existsByActivityIdAndUserId(activity.getId(), userId));
|
||||||
|
} else {
|
||||||
|
dto.setLikedByCurrentUser(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto;
|
||||||
})
|
})
|
||||||
.filter(dto -> dto != null)
|
.filter(dto -> dto != null)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
@ -141,12 +164,21 @@ public class TimelineService {
|
||||||
Page<Activity> activities = activityRepository.findByUserIdOrderByStartedAtDesc(userId, pageable);
|
Page<Activity> activities = activityRepository.findByUserIdOrderByStartedAtDesc(userId, pageable);
|
||||||
|
|
||||||
List<TimelineActivityDTO> timelineActivities = activities.getContent().stream()
|
List<TimelineActivityDTO> timelineActivities = activities.getContent().stream()
|
||||||
.map(activity -> TimelineActivityDTO.fromActivity(
|
.map(activity -> {
|
||||||
|
TimelineActivityDTO dto = TimelineActivityDTO.fromActivity(
|
||||||
activity,
|
activity,
|
||||||
user.getUsername(),
|
user.getUsername(),
|
||||||
user.getDisplayName() != null ? user.getDisplayName() : user.getUsername(),
|
user.getDisplayName() != null ? user.getDisplayName() : user.getUsername(),
|
||||||
user.getAvatarUrl()
|
user.getAvatarUrl()
|
||||||
))
|
);
|
||||||
|
|
||||||
|
// Add social interaction counts
|
||||||
|
dto.setLikesCount(likeRepository.countByActivityId(activity.getId()));
|
||||||
|
dto.setCommentsCount(commentRepository.countByActivityIdAndNotDeleted(activity.getId()));
|
||||||
|
dto.setLikedByCurrentUser(likeRepository.existsByActivityIdAndUserId(activity.getId(), userId));
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
return new PageImpl<>(timelineActivities, pageable, activities.getTotalElements());
|
return new PageImpl<>(timelineActivities, pageable, activities.getTotalElements());
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,8 @@ const FitPubTimeline = {
|
||||||
switch (this.timelineType) {
|
switch (this.timelineType) {
|
||||||
case 'public':
|
case 'public':
|
||||||
endpoint = `/api/timeline/public?page=${page}&size=20`;
|
endpoint = `/api/timeline/public?page=${page}&size=20`;
|
||||||
|
// Public timeline is optionally authenticated
|
||||||
|
fetchOptions = { useAuth: FitPubAuth.isAuthenticated() };
|
||||||
break;
|
break;
|
||||||
case 'federated':
|
case 'federated':
|
||||||
endpoint = `/api/timeline/federated?page=${page}&size=20`;
|
endpoint = `/api/timeline/federated?page=${page}&size=20`;
|
||||||
|
|
@ -178,14 +180,25 @@ const FitPubTimeline = {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Activity Actions -->
|
<!-- Activity Actions -->
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2 align-items-center">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm ${activity.likedByCurrentUser ? 'btn-danger' : 'btn-outline-danger'} like-btn"
|
||||||
|
data-activity-id="${activity.id}"
|
||||||
|
data-liked="${activity.likedByCurrentUser || false}"
|
||||||
|
>
|
||||||
|
<i class="bi bi-heart${activity.likedByCurrentUser ? '-fill' : ''}"></i>
|
||||||
|
<span class="like-count">${activity.likesCount || 0}</span>
|
||||||
|
</button>
|
||||||
<a href="/activities/${activity.id}" class="btn btn-sm btn-outline-primary">
|
<a href="/activities/${activity.id}" class="btn btn-sm btn-outline-primary">
|
||||||
<i class="bi bi-eye"></i> View Details
|
<i class="bi bi-eye"></i> View Details
|
||||||
</a>
|
</a>
|
||||||
<span class="ms-auto text-muted small">
|
<span class="ms-auto text-muted small d-flex align-items-center gap-2">
|
||||||
|
${activity.commentsCount > 0 ? `<span><i class="bi bi-chat-left-text"></i> ${activity.commentsCount}</span>` : ''}
|
||||||
|
<span>
|
||||||
<i class="bi bi-${this.getVisibilityIcon(activity.visibility)}"></i>
|
<i class="bi bi-${this.getVisibilityIcon(activity.visibility)}"></i>
|
||||||
${activity.visibility}
|
${activity.visibility}
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -198,6 +211,82 @@ const FitPubTimeline = {
|
||||||
this.renderPreviewMap(activity);
|
this.renderPreviewMap(activity);
|
||||||
});
|
});
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
|
// Setup like button handlers
|
||||||
|
this.setupLikeButtons();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup like button click handlers
|
||||||
|
*/
|
||||||
|
setupLikeButtons: function() {
|
||||||
|
const likeButtons = document.querySelectorAll('.like-btn');
|
||||||
|
|
||||||
|
likeButtons.forEach(btn => {
|
||||||
|
btn.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Check if user is authenticated
|
||||||
|
if (!FitPubAuth.isAuthenticated()) {
|
||||||
|
window.location.href = '/login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activityId = btn.dataset.activityId;
|
||||||
|
const isLiked = btn.dataset.liked === 'true';
|
||||||
|
const icon = btn.querySelector('i');
|
||||||
|
const countSpan = btn.querySelector('.like-count');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Disable button during request
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
if (isLiked) {
|
||||||
|
// Unlike
|
||||||
|
const response = await FitPubAuth.authenticatedFetch(
|
||||||
|
`/api/activities/${activityId}/likes`,
|
||||||
|
{ method: 'DELETE' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Update UI
|
||||||
|
btn.classList.remove('btn-danger');
|
||||||
|
btn.classList.add('btn-outline-danger');
|
||||||
|
icon.classList.remove('bi-heart-fill');
|
||||||
|
icon.classList.add('bi-heart');
|
||||||
|
btn.dataset.liked = 'false';
|
||||||
|
|
||||||
|
// Update count
|
||||||
|
const currentCount = parseInt(countSpan.textContent) || 0;
|
||||||
|
countSpan.textContent = Math.max(0, currentCount - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Like
|
||||||
|
const response = await FitPubAuth.authenticatedFetch(
|
||||||
|
`/api/activities/${activityId}/likes`,
|
||||||
|
{ method: 'POST' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Update UI
|
||||||
|
btn.classList.remove('btn-outline-danger');
|
||||||
|
btn.classList.add('btn-danger');
|
||||||
|
icon.classList.remove('bi-heart');
|
||||||
|
icon.classList.add('bi-heart-fill');
|
||||||
|
btn.dataset.liked = 'true';
|
||||||
|
|
||||||
|
// Update count
|
||||||
|
const currentCount = parseInt(countSpan.textContent) || 0;
|
||||||
|
countSpan.textContent = currentCount + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling like:', error);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,76 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Social Interactions -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="bi bi-chat-heart"></i> Social
|
||||||
|
</h5>
|
||||||
|
<div>
|
||||||
|
<button id="likeBtn" class="btn btn-sm btn-outline-danger me-2">
|
||||||
|
<i class="bi bi-heart"></i>
|
||||||
|
<span id="likeBtnText">Like</span>
|
||||||
|
(<span id="likeCount">0</span>)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Likes Section -->
|
||||||
|
<div class="mb-4" id="likesSection">
|
||||||
|
<h6 class="text-muted mb-3">
|
||||||
|
<i class="bi bi-heart-fill text-danger"></i>
|
||||||
|
Liked by <span id="likesCountText">0</span>
|
||||||
|
</h6>
|
||||||
|
<div id="likesList" class="d-flex flex-wrap gap-2">
|
||||||
|
<!-- Likes will be populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Comments Section -->
|
||||||
|
<div>
|
||||||
|
<h6 class="text-muted mb-3">
|
||||||
|
<i class="bi bi-chat-left-text"></i>
|
||||||
|
Comments (<span id="commentsCount">0</span>)
|
||||||
|
</h6>
|
||||||
|
|
||||||
|
<!-- Comment Form -->
|
||||||
|
<div id="commentForm" class="mb-4" style="display: none;">
|
||||||
|
<form id="addCommentForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<textarea
|
||||||
|
id="commentContent"
|
||||||
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Write a comment..."
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">
|
||||||
|
<i class="bi bi-send"></i> Post Comment
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Comments List -->
|
||||||
|
<div id="commentsList">
|
||||||
|
<!-- Comments will be populated here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Login prompt for non-authenticated users -->
|
||||||
|
<div id="loginPrompt" style="display: none;">
|
||||||
|
<p class="text-muted">
|
||||||
|
<a href="/login">Log in</a> to like or comment on this activity.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Back Button -->
|
<!-- Back Button -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|
@ -287,6 +357,11 @@
|
||||||
|
|
||||||
// Additional metrics
|
// Additional metrics
|
||||||
renderAdditionalMetrics(activity);
|
renderAdditionalMetrics(activity);
|
||||||
|
|
||||||
|
// Load social interactions
|
||||||
|
loadLikes();
|
||||||
|
loadComments();
|
||||||
|
setupSocialInteractions(activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMap(simplifiedTrack) {
|
function renderMap(simplifiedTrack) {
|
||||||
|
|
@ -415,6 +490,247 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Social interactions functionality
|
||||||
|
async function loadLikes() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/activities/${activityId}/likes`);
|
||||||
|
if (response.ok) {
|
||||||
|
const likes = await response.json();
|
||||||
|
renderLikes(likes);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading likes:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLikes(likes) {
|
||||||
|
const likesList = document.getElementById('likesList');
|
||||||
|
const likeCount = document.getElementById('likeCount');
|
||||||
|
const likesCountText = document.getElementById('likesCountText');
|
||||||
|
|
||||||
|
likeCount.textContent = likes.length;
|
||||||
|
likesCountText.textContent = likes.length;
|
||||||
|
|
||||||
|
if (likes.length === 0) {
|
||||||
|
likesList.innerHTML = '<span class="text-muted">No likes yet</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
likesList.innerHTML = likes.map(like => {
|
||||||
|
const displayName = like.displayName || like.username || 'Unknown';
|
||||||
|
const avatarHtml = like.avatarUrl
|
||||||
|
? `<img src="${like.avatarUrl}" alt="${displayName}" class="rounded-circle me-2" width="32" height="32">`
|
||||||
|
: `<i class="bi bi-person-circle me-2" style="font-size: 32px;"></i>`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="d-flex align-items-center p-2 border rounded">
|
||||||
|
${avatarHtml}
|
||||||
|
<span>${displayName}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadComments() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/activities/${activityId}/comments`);
|
||||||
|
if (response.ok) {
|
||||||
|
const commentsPage = await response.json();
|
||||||
|
renderComments(commentsPage.content || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading comments:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderComments(comments) {
|
||||||
|
const commentsList = document.getElementById('commentsList');
|
||||||
|
const commentsCount = document.getElementById('commentsCount');
|
||||||
|
|
||||||
|
commentsCount.textContent = comments.length;
|
||||||
|
|
||||||
|
if (comments.length === 0) {
|
||||||
|
commentsList.innerHTML = '<p class="text-muted">No comments yet. Be the first to comment!</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
commentsList.innerHTML = comments.map(comment => {
|
||||||
|
const displayName = comment.displayName || comment.username || 'Unknown';
|
||||||
|
const avatarHtml = comment.avatarUrl
|
||||||
|
? `<img src="${comment.avatarUrl}" alt="${displayName}" class="rounded-circle" width="40" height="40">`
|
||||||
|
: `<i class="bi bi-person-circle" style="font-size: 40px;"></i>`;
|
||||||
|
|
||||||
|
const createdAt = new Date(comment.createdAt).toLocaleString();
|
||||||
|
const deleteBtn = comment.canDelete
|
||||||
|
? `<button class="btn btn-sm btn-outline-danger delete-comment-btn" data-comment-id="${comment.id}">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="d-flex mb-3 pb-3 border-bottom" data-comment-id="${comment.id}">
|
||||||
|
<div class="me-3">
|
||||||
|
${avatarHtml}
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div>
|
||||||
|
<strong>${displayName}</strong>
|
||||||
|
<small class="text-muted ms-2">${createdAt}</small>
|
||||||
|
</div>
|
||||||
|
${deleteBtn}
|
||||||
|
</div>
|
||||||
|
<p class="mb-0 mt-1">${escapeHtml(comment.content)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// Add delete event listeners
|
||||||
|
document.querySelectorAll('.delete-comment-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async function() {
|
||||||
|
if (confirm('Delete this comment?')) {
|
||||||
|
await deleteComment(this.dataset.commentId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupSocialInteractions(activity) {
|
||||||
|
const isAuthenticated = FitPubAuth.isAuthenticated();
|
||||||
|
|
||||||
|
if (isAuthenticated) {
|
||||||
|
// Show comment form for authenticated users
|
||||||
|
document.getElementById('commentForm').style.display = 'block';
|
||||||
|
|
||||||
|
// Update like button based on activity data
|
||||||
|
if (activity.likedByCurrentUser) {
|
||||||
|
updateLikeButton(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup like button click handler
|
||||||
|
document.getElementById('likeBtn').addEventListener('click', handleLikeClick);
|
||||||
|
|
||||||
|
// Setup comment form submit handler
|
||||||
|
document.getElementById('addCommentForm').addEventListener('submit', handleCommentSubmit);
|
||||||
|
} else {
|
||||||
|
// Show login prompt for non-authenticated users
|
||||||
|
document.getElementById('loginPrompt').style.display = 'block';
|
||||||
|
document.getElementById('likeBtn').disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLikeClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const btn = event.currentTarget;
|
||||||
|
const isLiked = btn.classList.contains('btn-danger');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isLiked) {
|
||||||
|
// Unlike
|
||||||
|
const response = await FitPubAuth.authenticatedFetch(
|
||||||
|
`/api/activities/${activityId}/likes`,
|
||||||
|
{ method: 'DELETE' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
updateLikeButton(false);
|
||||||
|
loadLikes(); // Reload likes list
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Like
|
||||||
|
const response = await FitPubAuth.authenticatedFetch(
|
||||||
|
`/api/activities/${activityId}/likes`,
|
||||||
|
{ method: 'POST' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
updateLikeButton(true);
|
||||||
|
loadLikes(); // Reload likes list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling like:', error);
|
||||||
|
FitPub.showAlert('Failed to update like. Please try again.', 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLikeButton(isLiked) {
|
||||||
|
const btn = document.getElementById('likeBtn');
|
||||||
|
const btnText = document.getElementById('likeBtnText');
|
||||||
|
const icon = btn.querySelector('i');
|
||||||
|
|
||||||
|
if (isLiked) {
|
||||||
|
btn.classList.remove('btn-outline-danger');
|
||||||
|
btn.classList.add('btn-danger');
|
||||||
|
icon.classList.remove('bi-heart');
|
||||||
|
icon.classList.add('bi-heart-fill');
|
||||||
|
btnText.textContent = 'Liked';
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('btn-danger');
|
||||||
|
btn.classList.add('btn-outline-danger');
|
||||||
|
icon.classList.remove('bi-heart-fill');
|
||||||
|
icon.classList.add('bi-heart');
|
||||||
|
btnText.textContent = 'Like';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCommentSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const contentInput = document.getElementById('commentContent');
|
||||||
|
const content = contentInput.value.trim();
|
||||||
|
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await FitPubAuth.authenticatedFetch(
|
||||||
|
`/api/activities/${activityId}/comments`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ content })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
contentInput.value = '';
|
||||||
|
loadComments(); // Reload comments list
|
||||||
|
FitPub.showAlert('Comment posted successfully', 'success');
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to post comment');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error posting comment:', error);
|
||||||
|
FitPub.showAlert('Failed to post comment. Please try again.', 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteComment(commentId) {
|
||||||
|
try {
|
||||||
|
const response = await FitPubAuth.authenticatedFetch(
|
||||||
|
`/api/activities/${activityId}/comments/${commentId}`,
|
||||||
|
{ method: 'DELETE' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
loadComments(); // Reload comments list
|
||||||
|
FitPub.showAlert('Comment deleted successfully', 'success');
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to delete comment');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting comment:', error);
|
||||||
|
FitPub.showAlert('Failed to delete comment. Please try again.', 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
// Delete functionality
|
// Delete functionality
|
||||||
document.getElementById('deleteBtn').addEventListener('click', function() {
|
document.getElementById('deleteBtn').addEventListener('click', function() {
|
||||||
const modal = new bootstrap.Modal(document.getElementById('deleteModal'));
|
const modal = new bootstrap.Modal(document.getElementById('deleteModal'));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue