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.
|
||||
* 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
|
||||
*
|
||||
* @param userDetails the authenticated user details (optional)
|
||||
* @param page page number (default: 0)
|
||||
* @param size page size (default: 20)
|
||||
* @return page of timeline activities
|
||||
*/
|
||||
@GetMapping("/public")
|
||||
public ResponseEntity<Page<TimelineActivityDTO>> getPublicTimeline(
|
||||
@AuthenticationPrincipal(errorOnInvalidType = false) UserDetails userDetails,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@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);
|
||||
Page<TimelineActivityDTO> timeline = timelineService.getPublicTimeline(pageable);
|
||||
Page<TimelineActivityDTO> timeline = timelineService.getPublicTimeline(userId, pageable);
|
||||
|
||||
return ResponseEntity.ok(timeline);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ public class TimelineActivityDTO {
|
|||
private String avatarUrl;
|
||||
private boolean isLocal;
|
||||
|
||||
// Social interaction counts
|
||||
private Long likesCount;
|
||||
private Long commentsCount;
|
||||
private Boolean likedByCurrentUser;
|
||||
|
||||
// Metrics summary
|
||||
private ActivityMetricsSummary metrics;
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ public class TimelineService {
|
|||
private final ActivityRepository activityRepository;
|
||||
private final FollowRepository followRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final org.operaton.fitpub.repository.LikeRepository likeRepository;
|
||||
private final org.operaton.fitpub.repository.CommentRepository commentRepository;
|
||||
|
||||
@Value("${fitpub.base-url}")
|
||||
private String baseUrl;
|
||||
|
|
@ -74,12 +76,19 @@ public class TimelineService {
|
|||
if (activityUser == null) {
|
||||
return null;
|
||||
}
|
||||
return TimelineActivityDTO.fromActivity(
|
||||
TimelineActivityDTO dto = TimelineActivityDTO.fromActivity(
|
||||
activity,
|
||||
activityUser.getUsername(),
|
||||
activityUser.getDisplayName() != null ? activityUser.getDisplayName() : activityUser.getUsername(),
|
||||
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)
|
||||
.collect(Collectors.toList());
|
||||
|
|
@ -91,11 +100,12 @@ public class TimelineService {
|
|||
* Get the public timeline.
|
||||
* Shows all public activities from all users.
|
||||
*
|
||||
* @param userId optional user ID for checking liked status (null for unauthenticated)
|
||||
* @param pageable pagination parameters
|
||||
* @return page of timeline activities
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Page<TimelineActivityDTO> getPublicTimeline(Pageable pageable) {
|
||||
public Page<TimelineActivityDTO> getPublicTimeline(UUID userId, Pageable pageable) {
|
||||
log.debug("Fetching public timeline");
|
||||
|
||||
// Fetch all public activities
|
||||
|
|
@ -111,12 +121,25 @@ public class TimelineService {
|
|||
if (activityUser == null) {
|
||||
return null;
|
||||
}
|
||||
return TimelineActivityDTO.fromActivity(
|
||||
TimelineActivityDTO dto = TimelineActivityDTO.fromActivity(
|
||||
activity,
|
||||
activityUser.getUsername(),
|
||||
activityUser.getDisplayName() != null ? activityUser.getDisplayName() : activityUser.getUsername(),
|
||||
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)
|
||||
.collect(Collectors.toList());
|
||||
|
|
@ -141,12 +164,21 @@ public class TimelineService {
|
|||
Page<Activity> activities = activityRepository.findByUserIdOrderByStartedAtDesc(userId, pageable);
|
||||
|
||||
List<TimelineActivityDTO> timelineActivities = activities.getContent().stream()
|
||||
.map(activity -> TimelineActivityDTO.fromActivity(
|
||||
activity,
|
||||
user.getUsername(),
|
||||
user.getDisplayName() != null ? user.getDisplayName() : user.getUsername(),
|
||||
user.getAvatarUrl()
|
||||
))
|
||||
.map(activity -> {
|
||||
TimelineActivityDTO dto = TimelineActivityDTO.fromActivity(
|
||||
activity,
|
||||
user.getUsername(),
|
||||
user.getDisplayName() != null ? user.getDisplayName() : user.getUsername(),
|
||||
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());
|
||||
|
||||
return new PageImpl<>(timelineActivities, pageable, activities.getTotalElements());
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ const FitPubTimeline = {
|
|||
switch (this.timelineType) {
|
||||
case 'public':
|
||||
endpoint = `/api/timeline/public?page=${page}&size=20`;
|
||||
// Public timeline is optionally authenticated
|
||||
fetchOptions = { useAuth: FitPubAuth.isAuthenticated() };
|
||||
break;
|
||||
case 'federated':
|
||||
endpoint = `/api/timeline/federated?page=${page}&size=20`;
|
||||
|
|
@ -178,13 +180,24 @@ const FitPubTimeline = {
|
|||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<i class="bi bi-eye"></i> View Details
|
||||
</a>
|
||||
<span class="ms-auto text-muted small">
|
||||
<i class="bi bi-${this.getVisibilityIcon(activity.visibility)}"></i>
|
||||
${activity.visibility}
|
||||
<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>
|
||||
${activity.visibility}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -198,6 +211,82 @@ const FitPubTimeline = {
|
|||
this.renderPreviewMap(activity);
|
||||
});
|
||||
}, 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>
|
||||
|
||||
<!-- 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 -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
|
|
@ -287,6 +357,11 @@
|
|||
|
||||
// Additional metrics
|
||||
renderAdditionalMetrics(activity);
|
||||
|
||||
// Load social interactions
|
||||
loadLikes();
|
||||
loadComments();
|
||||
setupSocialInteractions(activity);
|
||||
}
|
||||
|
||||
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
|
||||
document.getElementById('deleteBtn').addEventListener('click', function() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('deleteModal'));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue