516 lines
18 KiB
Svelte
516 lines
18 KiB
Svelte
<script lang='ts'>
|
|
|
|
import {getImageURL, formatDate} from '$lib/utils';
|
|
import toast from 'svelte-french-toast';
|
|
import { commentAdded, commentIdDeleted, screenWidth, replyCommentClicked, editCommentClicked, scrollToComment } from '$lib/store';
|
|
import TextEditor from '$lib/components/TextEditor.svelte';
|
|
import { marked } from 'marked';
|
|
import { tick } from 'svelte';
|
|
|
|
export let moderators
|
|
export let comment;
|
|
export let data;
|
|
export let postId;
|
|
export let opUserId;
|
|
|
|
export let upvoteButtonClicked
|
|
export let downvoteButtonClicked
|
|
export let upvoteCounter;
|
|
export let downvoteCounter;
|
|
export let userAlreadyVoted;
|
|
|
|
|
|
if (userAlreadyVoted) {
|
|
upvoteButtonClicked = comment?.expand['alreadyVoted(comment)']?.find(item => item?.user === data?.user?.id)?.type === 'upvote';
|
|
downvoteButtonClicked = comment?.expand['alreadyVoted(comment)']?.find(item => item?.user === data?.user?.id)?.type === 'downvote';
|
|
} else {
|
|
upvoteButtonClicked = false;
|
|
downvoteButtonClicked = false;
|
|
}
|
|
|
|
|
|
|
|
function removeDuplicateClasses(str) {
|
|
return str.replace(/class="([^"]*)"/g, (match, classAttr) => {
|
|
return `class="${[...new Set(classAttr.split(' '))].join(' ')}"`;
|
|
});
|
|
}
|
|
|
|
function addClassesToHtml(htmlString) {
|
|
// Helper function to add a class to a specific tag
|
|
function addClassToTag(tag, className) {
|
|
// Add class if the tag doesn't already have a class attribute
|
|
const regex = new RegExp(`<${tag}(?![^>]*\\bclass=)([^>]*)>`, 'g');
|
|
htmlString = htmlString.replace(regex, `<${tag} class="${className}"$1>`);
|
|
|
|
// Append the new class to tags that already have a class attribute, ensuring no duplicates
|
|
const regexWithClass = new RegExp(`(<${tag}[^>]*\\bclass=["'][^"']*)(?!.*\\b${className}\\b)([^"']*)["']`, 'g');
|
|
htmlString = htmlString.replace(regexWithClass, `$1 ${className}$2"`);
|
|
}
|
|
|
|
// Add classes to headings
|
|
addClassToTag('h1', 'text-lg');
|
|
addClassToTag('h2', 'text-lg');
|
|
addClassToTag('h3', 'text-lg');
|
|
addClassToTag('h4', 'text-lg');
|
|
addClassToTag('h5', 'text-lg');
|
|
addClassToTag('h6', 'text-lg');
|
|
|
|
// Add classes to anchor tags
|
|
addClassToTag('a', 'text-blue-400 hover:text-white underline');
|
|
|
|
// Add classes to ordered lists
|
|
addClassToTag('ol', 'list-decimal ml-10 text-sm');
|
|
|
|
// Add classes to unordered lists
|
|
addClassToTag('ul', 'list-disc ml-10 text-sm -mt-5');
|
|
|
|
// Add classes to blockquotes and their paragraphs
|
|
function addClassToBlockquote() {
|
|
// Add class to blockquote
|
|
htmlString = htmlString.replace(
|
|
/<blockquote/g,
|
|
'<blockquote class="pl-4 pr-4 rounded-lg bg-[#323232]"'
|
|
);
|
|
|
|
// Add class to p inside blockquote
|
|
htmlString = htmlString.replace(
|
|
/<blockquote([^>]*)>\s*<p/g,
|
|
`<blockquote$1>\n<p class="text-sm font-medium leading-relaxed text-white"`
|
|
);
|
|
}
|
|
|
|
addClassToBlockquote();
|
|
|
|
// Remove duplicate classes after all modifications
|
|
htmlString = removeDuplicateClasses(htmlString);
|
|
|
|
return htmlString;
|
|
}
|
|
|
|
|
|
|
|
const handleUpvote = async (event) => {
|
|
|
|
event.preventDefault(); // prevent the default form submission behavior
|
|
|
|
const commentId = event.target.commentId.value;
|
|
const postData = {
|
|
'postId': postId,
|
|
'commentId': commentId,
|
|
'userId': data?.user?.id,
|
|
'path': 'upvote-comment'
|
|
};
|
|
|
|
upvoteButtonClicked = !upvoteButtonClicked;
|
|
|
|
|
|
if (upvoteButtonClicked) {
|
|
if (downvoteButtonClicked) {
|
|
upvoteCounter += 1;
|
|
downvoteCounter -= 1;
|
|
downvoteButtonClicked = false;
|
|
} else {
|
|
upvoteCounter++;
|
|
}
|
|
} else {
|
|
upvoteCounter--;
|
|
}
|
|
const response = await fetch('/api/fastify-post-data', {
|
|
method: 'POST',
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(postData)
|
|
}); // make a POST request to the server with the FormData object
|
|
|
|
};
|
|
|
|
|
|
const handleDownvote = async (event) => {
|
|
event.preventDefault(); // prevent the default form submission behavior
|
|
|
|
const commentId = event.target.commentId.value;
|
|
const postData = {
|
|
'commentId': commentId,
|
|
'userId': data?.user?.id,
|
|
'path': 'downvote-comment'
|
|
};
|
|
|
|
downvoteButtonClicked = !downvoteButtonClicked;
|
|
|
|
|
|
if (downvoteButtonClicked) {
|
|
if (upvoteButtonClicked) {
|
|
downvoteCounter += 1;
|
|
upvoteCounter -= 1;
|
|
upvoteButtonClicked = false;
|
|
} else {
|
|
downvoteCounter++;
|
|
}
|
|
} else {
|
|
downvoteCounter--;
|
|
}
|
|
|
|
|
|
const response = await fetch('/api/fastify-post-data', {
|
|
method: 'POST',
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(postData)
|
|
}); // make a POST request to the server with the FormData object
|
|
|
|
};
|
|
|
|
|
|
let deleteCommentId = comment?.id
|
|
|
|
function isModerator(comment) {
|
|
return moderators?.some(moderator => comment?.user === moderator?.user);
|
|
}
|
|
|
|
|
|
function repeatedCharacters(str) {
|
|
// This regex matches any character (.) followed by itself at least five times
|
|
const regex = /(.)\1{10,}/;
|
|
|
|
// Test the string against the regex
|
|
return regex?.test(str);
|
|
}
|
|
|
|
|
|
const handleDeleteComment = async () => {
|
|
|
|
|
|
const postData = {
|
|
'userId': data?.user?.id,
|
|
'commentId': comment?.id,
|
|
'commentUser': comment?.user,
|
|
'path': 'delete-comment'
|
|
}
|
|
|
|
|
|
|
|
const response = await fetch('/api/fastify-post-data', {
|
|
method: 'POST',
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(postData)
|
|
}); // make a POST request to the server with the FormData object
|
|
|
|
const output = (await response.json())?.message;
|
|
if (output === 'success')
|
|
{
|
|
$commentIdDeleted = comment?.id;
|
|
|
|
}
|
|
|
|
if (output === 'success') {
|
|
toast.success('Comment deleted', {
|
|
style: 'border-radius: 200px; background: #333; color: #fff;'
|
|
});
|
|
} else {
|
|
toast.error('Something went wrong', {
|
|
style: 'border-radius: 200px; background: #333; color: #fff;'
|
|
});
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleReportComment = async () => {
|
|
|
|
toast.success('Comment reported.', {
|
|
style: 'border-radius: 200px; background: #333; color: #fff'
|
|
});
|
|
|
|
|
|
}
|
|
|
|
const toggle = (state) => {
|
|
if (state === 'reply') {
|
|
$replyCommentClicked[comment.id] = !$replyCommentClicked[comment.id]
|
|
$editCommentClicked[comment.id] = false;
|
|
}
|
|
else if (state === 'edit') {
|
|
$editCommentClicked[comment.id] = !$editCommentClicked[comment.id];
|
|
$replyCommentClicked[comment.id] = false;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
$replyCommentClicked[comment?.id] = false
|
|
$editCommentClicked[comment?.id] = false
|
|
|
|
|
|
$: if ($scrollToComment?.length !== 0 && typeof window !== 'undefined') {
|
|
// Wait for the DOM to update
|
|
tick().then(() => {
|
|
const commentElement = document.getElementById($scrollToComment);
|
|
if (commentElement) {
|
|
commentElement.scrollIntoView({ behavior: 'smooth', inline: 'nearest', block: "center"});
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
$: {
|
|
if($commentAdded?.length !== 0) {
|
|
console.log('yes')
|
|
}
|
|
}
|
|
|
|
|
|
$: {
|
|
if($commentIdDeleted === comment?.id)
|
|
{
|
|
upvoteCounter = {};
|
|
downvoteCounter= {};
|
|
upvoteButtonClicked= {};
|
|
downvoteButtonClicked= {};
|
|
}
|
|
}
|
|
|
|
*/
|
|
|
|
|
|
</script>
|
|
|
|
|
|
<div class="comment border-l border-gray-500 mt-8">
|
|
<div class="flex flex-row justify-start items-center -ml-4">
|
|
<a href={'/community/user/'+comment?.expand?.user?.id} class="flex flex-row items-center justify-start">
|
|
<label class="avatar w-7 h-7 flex-shrink-0 text-white text-xs sm:text-sm ml-1">
|
|
<img class="flex-shrink-0 inline-block bg-slate-300 rounded-full"
|
|
src={comment?.expand?.user?.avatar
|
|
? getImageURL(comment?.expand?.user?.collectionId, comment?.expand?.user?.id, comment?.expand?.user?.avatar)
|
|
: `https://avatar.vercel.sh/${comment?.expand?.user?.username}`}
|
|
alt="User avatar" />
|
|
</label>
|
|
<span class="text-white ml-2 inline-block text-xs sm:text-sm">
|
|
{comment?.expand?.user?.username}
|
|
</span>
|
|
{#if isModerator(comment)}
|
|
<svg class="inline-block ml-1 w-3 h-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#75d377" d="M256 32C174 69.06 121.38 86.46 32 96c0 77.59 5.27 133.36 25.29 184.51a348.86 348.86 0 0 0 71.43 112.41c49.6 52.66 104.17 80.4 127.28 87.08c23.11-6.68 77.68-34.42 127.28-87.08a348.86 348.86 0 0 0 71.43-112.41C474.73 229.36 480 173.59 480 96c-89.38-9.54-142-26.94-224-64Z"/></svg>
|
|
{/if}
|
|
{#if comment?.user === opUserId}
|
|
<span class="text-[#756EFF] text-sm font-semibold ml-1">
|
|
OP
|
|
</span>
|
|
{/if}
|
|
<span class="text-white font-bold ml-1 mr-1">
|
|
·
|
|
</span>
|
|
<span class="text-white text-xs">
|
|
{formatDate(comment?.created)} ago
|
|
</span>
|
|
</a>
|
|
|
|
|
|
|
|
</div>
|
|
<div class="text-md text-slate-400 mb-1 pl-5 pt-3 whitespace-pre-wrap w-11/12 sm:w-5/6 ">
|
|
|
|
|
|
<div id={comment?.id} class="text-sm text-[#D7DADC] rounded-lg {comment?.id === $scrollToComment ? 'pt-3 pl-3 pr-3 mb-5 bg-[#31304D]' : ''} whitespace-pre-line {repeatedCharacters(comment?.comment) === true ? 'break-all' : ''}">
|
|
{#if !$editCommentClicked[comment?.id]}
|
|
{@html addClassesToHtml(marked(comment?.comment))}
|
|
{:else}
|
|
<TextEditor
|
|
data={data}
|
|
postId={postId}
|
|
commentId={comment?.id}
|
|
inputValue={comment?.comment}
|
|
placeholder={"Reply to the comment of @"+comment?.expand?.user?.username+"..."}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
{#if comment?.image?.length !== 0}
|
|
<div class="relative mr-auto -mt-5">
|
|
<img
|
|
src={getImageURL(comment?.collectionId, comment?.id, comment?.image)}
|
|
class="w-auto max-w-36 max-h-[350px] sm:max-h-[550px] mr-auto"
|
|
alt="comment Image"
|
|
/>
|
|
</div>
|
|
{/if}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex flex-col items-start w-full">
|
|
<div class="pl-5 flex flex-row items-center {comment?.image?.length === 0 ? '-mt-8' : ''} ">
|
|
<!--Start Voting-->
|
|
<!--Start Upvote -->
|
|
<form on:submit={handleUpvote}>
|
|
<input type="hidden" name="commentId" value={comment?.id}>
|
|
{#if !data?.user}
|
|
<label for="userLogin" class="text-[#A6ADBB] cursor-pointer rounded-lg w-8 h-8 relative sm:hover:bg-[#333333] flex items-center justify-center">
|
|
<svg class="rotate-180 w-4 h-4" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512.171 512.171" xml:space="preserve"><path fill="currentColor" d="M479.046,283.925c-1.664-3.989-5.547-6.592-9.856-6.592H352.305V10.667C352.305,4.779,347.526,0,341.638,0H170.971 c-5.888,0-10.667,4.779-10.667,10.667v266.667H42.971c-4.309,0-8.192,2.603-9.856,6.571c-1.643,3.989-0.747,8.576,2.304,11.627 l212.8,213.504c2.005,2.005,4.715,3.136,7.552,3.136s5.547-1.131,7.552-3.115l213.419-213.504 C479.793,292.501,480.71,287.915,479.046,283.925z"></path></svg>
|
|
</label>
|
|
{:else}
|
|
<button type="submit" class="{upvoteButtonClicked ? 'text-[#0076FE] bg-[#31304D]' : 'text-[#A6ADBB]'} cursor-pointer rounded-lg w-8 h-8 relative sm:hover:bg-[#333333] flex items-center justify-center">
|
|
<svg class="rotate-180 w-4 h-4" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512.171 512.171" xml:space="preserve"><path fill="currentColor" d="M479.046,283.925c-1.664-3.989-5.547-6.592-9.856-6.592H352.305V10.667C352.305,4.779,347.526,0,341.638,0H170.971 c-5.888,0-10.667,4.779-10.667,10.667v266.667H42.971c-4.309,0-8.192,2.603-9.856,6.571c-1.643,3.989-0.747,8.576,2.304,11.627 l212.8,213.504c2.005,2.005,4.715,3.136,7.552,3.136s5.547-1.131,7.552-3.115l213.419-213.504 C479.793,292.501,480.71,287.915,479.046,283.925z"></path></svg>
|
|
</button >
|
|
{/if}
|
|
</form>
|
|
<!--End Upvote-->
|
|
<!--Start Downvote-->
|
|
<span class="text-gray-200 text-sm ml-1.5 mr-1.5">
|
|
{upvoteCounter - downvoteCounter }
|
|
</span>
|
|
<form on:submit={handleDownvote}>
|
|
<input type="hidden" name="commentId" value={comment?.id} />
|
|
{#if !data?.user}
|
|
<label for="userLogin" class="mr-2 cursor-pointer rounded-lg w-8 h-8 relative sm:hover:bg-[#333333] flex items-center justify-center">
|
|
<svg class="w-4 h-4 mt-0.5" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512.171 512.171" xml:space="preserve"><path fill="currentColor" d="M479.046,283.925c-1.664-3.989-5.547-6.592-9.856-6.592H352.305V10.667C352.305,4.779,347.526,0,341.638,0H170.971 c-5.888,0-10.667,4.779-10.667,10.667v266.667H42.971c-4.309,0-8.192,2.603-9.856,6.571c-1.643,3.989-0.747,8.576,2.304,11.627 l212.8,213.504c2.005,2.005,4.715,3.136,7.552,3.136s5.547-1.131,7.552-3.115l213.419-213.504 C479.793,292.501,480.71,287.915,479.046,283.925z"></path></svg>
|
|
</label>
|
|
{:else}
|
|
<button type="submit" class="{downvoteButtonClicked ? 'text-[#0076FE] bg-[#31304D]' : 'text-[#A6ADBB]'} mr-2 cursor-pointer rounded-lg w-8 h-8 relative sm:hover:bg-[#333333] flex items-center justify-center">
|
|
<svg class="w-4 h-4 mt-0.5" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512.171 512.171" xml:space="preserve"><path fill="currentColor" d="M479.046,283.925c-1.664-3.989-5.547-6.592-9.856-6.592H352.305V10.667C352.305,4.779,347.526,0,341.638,0H170.971 c-5.888,0-10.667,4.779-10.667,10.667v266.667H42.971c-4.309,0-8.192,2.603-9.856,6.571c-1.643,3.989-0.747,8.576,2.304,11.627 l212.8,213.504c2.005,2.005,4.715,3.136,7.552,3.136s5.547-1.131,7.552-3.115l213.419-213.504 C479.793,292.501,480.71,287.915,479.046,283.925z"></path></svg>
|
|
</button>
|
|
{/if}
|
|
</form>
|
|
<!--End Downvote-->
|
|
<!--End Voting-->
|
|
|
|
<label class="mr-3 cursor-pointer text-[12.5px] font-bold text-[#8C8C8C]" for={!data?.user ? 'userLogin' : ''} on:click={() => toggle('reply')}>
|
|
Reply
|
|
</label>
|
|
|
|
{#if data?.user?.id === comment?.expand?.user?.id}
|
|
<label class="mr-3 cursor-pointer text-[12.5px] font-bold text-[#8C8C8C]" for={!data?.user ? 'userLogin' : ''} on:click={() => toggle('edit')}>
|
|
Edit
|
|
</label>
|
|
{/if}
|
|
|
|
{#if data?.user?.id === comment?.expand?.user?.id || data?.user?.id === moderators?.at(0).user}
|
|
<label for={'delete'+deleteCommentId} class="cursor-pointer text-[12.5px] font-bold text-[#8C8C8C]">
|
|
Delete
|
|
</label>
|
|
{/if}
|
|
|
|
|
|
</div>
|
|
|
|
<div class="pl-8 w-full">
|
|
|
|
{#if $replyCommentClicked[comment?.id]}
|
|
<div class="mt-3 -ml-3">
|
|
{#if data?.user}
|
|
<TextEditor
|
|
data={data}
|
|
postId={postId}
|
|
commentId={comment?.id}
|
|
placeholder={"Reply to the comment of @"+comment?.expand?.user?.username+"..."}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
{#if comment?.children}
|
|
<div class="ml-2">
|
|
{#each comment.children as comment}
|
|
<svelte:self
|
|
{moderators}
|
|
{comment}
|
|
{data}
|
|
{postId}
|
|
{opUserId}
|
|
upvoteCounter={comment?.upvote}
|
|
downvoteCounter={comment?.downvote}
|
|
userAlreadyVoted={comment?.expand['alreadyVoted(comment)']?.some(item => item?.user === data?.user?.id)}
|
|
/>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
|
|
</div>
|
|
|
|
|
|
<!--Start Delete Modal-->
|
|
<input type="checkbox" id={'delete'+deleteCommentId} class="modal-toggle" />
|
|
|
|
<dialog id={'delete'+deleteCommentId} class="modal modal-bottom sm:modal-middle border border-slate-800">
|
|
|
|
<label for={'delete'+deleteCommentId} class="cursor-pointer modal-backdrop bg-[#fff] bg-opacity-[0.05]"></label>
|
|
|
|
<div class="modal-box bg-[#09090B] p-5 border border-slate-600 shadow-none" >
|
|
|
|
<h3 class="font-bold text-md sm:text-lg sm:mb-10 text-white mt-5">
|
|
Are you sure you want to delete the comment?
|
|
</h3>
|
|
|
|
<div class="modal-action pb-4">
|
|
<label for={'delete'+deleteCommentId} class="cursor-pointer text-sm px-3 py-3 rounded-lg m-auto text-white mr-5 bg-[#646464]">
|
|
No, cancel
|
|
</label>
|
|
<label on:click={handleDeleteComment} for={'delete'+deleteCommentId} class="cursor-pointer text-sm px-3 py-3 rounded-lg m-auto text-white mr-5 bg-purple-600">
|
|
Yes, I'm sure
|
|
</label>
|
|
|
|
</div>
|
|
</div>
|
|
</dialog>
|
|
<!--End Delete Modal-->
|
|
|
|
|
|
|
|
<!--Start Delete Modal-->
|
|
<input type="checkbox" id="reportComment" class="modal-toggle" />
|
|
|
|
<dialog id="reportComment" class="modal modal-bottom sm:modal-middle">
|
|
|
|
|
|
<div class="modal-box" >
|
|
<label for="reportComment" class="{$screenWidth < 640 ? 'hidden' : ''} btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</label>
|
|
|
|
<h3 class="font-bold text-md sm:text-lg sm:mb-10">
|
|
Are you sure you want to report the comment?
|
|
</h3>
|
|
|
|
<div class="modal-action">
|
|
<label for="reportComment" class="btn text-xs text-white mr-5">
|
|
No, cancel
|
|
</label>
|
|
<label on:click={handleReportComment} for="reportComment" class="btn bg-red-700 hover:bg-red-800 text-xs text-white mr-5">
|
|
Yes, I'm sure
|
|
</label>
|
|
|
|
</div>
|
|
</div>
|
|
</dialog>
|
|
<!--End Delete Modal-->
|
|
|
|
|
|
|
|
<style>
|
|
.comment {
|
|
margin-inline-start: 1rem;
|
|
}
|
|
.comment.lines {
|
|
position: relative;
|
|
padding-inline-start: 1rem;
|
|
}
|
|
|
|
/* Media query for mobile devices */
|
|
@media (max-width: 640px) {
|
|
.comment {
|
|
margin-inline-start: 0.5rem;
|
|
}
|
|
.comment.lines {
|
|
padding-inline-start: 0.5rem;
|
|
}
|
|
}
|
|
</style>
|
|
|