remove community related stuff and update profile page
This commit is contained in:
parent
3ccd3af1aa
commit
5694ea6ce9
@ -1,34 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
export let data;
|
||||
|
||||
function handleClick() {
|
||||
const label = document.getElementById("animated-label");
|
||||
label.classList.add("animate-bounce");
|
||||
|
||||
goto("/community/create-post");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="sm:hidden fixed z-50 w-full h-16 max-w-3xl -right-5 bottom-5">
|
||||
<div class="h-full max-w-3xl mx-auto">
|
||||
<div class="flex items-center justify-end mr-10">
|
||||
<label
|
||||
id="animated-label"
|
||||
on:click={handleClick}
|
||||
class="inline-flex items-center justify-center w-14 h-14 font-medium bg-[#fff] bg-opacity-[0.8] rounded-full cursor-pointer"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-white inline-block"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
><path
|
||||
fill="white"
|
||||
d="m362.7 19.3l-48.4 48.4l130 130l48.4-48.4c25-25 25-65.5 0-90.5l-39.4-39.5c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2c-2.5 8.5-.2 17.6 6 23.8s15.3 8.5 23.7 6.1L151 475.7c14.1-4.2 27-11.8 37.4-22.2l233.3-233.2l-130-130z"
|
||||
/></svg
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,598 +0,0 @@
|
||||
<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}
|
||||
{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}
|
||||
{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-[#fff]"
|
||||
>
|
||||
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>
|
||||
@ -1,46 +0,0 @@
|
||||
<script lang='ts'>
|
||||
import {getImageURL} from '$lib/utils';
|
||||
|
||||
export let data;
|
||||
|
||||
</script>
|
||||
|
||||
<!-- Create Post -->
|
||||
<!-- List container -->
|
||||
<div class="flex flex-col">
|
||||
<!-- Item -->
|
||||
<div class="sm:border border-gray-700 sm:hover:border-gray-600 rounded-none sm:rounded-md bg-[#141417] rounded-[4px] sm:rounded-[8px]">
|
||||
<div class="flex h-14 sm:h-16 justify-start items-center">
|
||||
<div class="ml-2 sm:ml-3 avatar relative">
|
||||
<div class="w-8 sm:w-10 rounded-full">
|
||||
<img src={data?.avatar
|
||||
? getImageURL(data?.collectionId, data?.id, data?.avatar)
|
||||
: `https://avatar.vercel.sh/${data?.user?.username}`} />
|
||||
</div>
|
||||
<div class="w-3.5 h-3.5 sm:w-4 sm:h-4 bg-green-400 border border-2 border-slate-800 rounded-full absolute bottom-0 right-0"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<a href="/community/create-post/" class="pl-3 sm:pl-5 text-white text-sm rounded-md h-10 sm:h-12 w-full bg-[#313131] cursor-pointer ml-3 flex justify-start items-center">
|
||||
Create Post
|
||||
</a>
|
||||
<div class="flex justify-start items-center ml-2">
|
||||
|
||||
<a href="/community/create-post" class="text-white mr-2">
|
||||
<svg class="w-6 h-6 inline-block ml-1 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path opacity="0.4" d="M22.0206 16.8198L18.8906 9.49978C18.3206 8.15978 17.4706 7.39978 16.5006 7.34978C15.5406 7.29978 14.6106 7.96978 13.9006 9.24978L12.0006 12.6598C11.6006 13.3798 11.0306 13.8098 10.4106 13.8598C9.78063 13.9198 9.15063 13.5898 8.64063 12.9398L8.42063 12.6598C7.71063 11.7698 6.83063 11.3398 5.93063 11.4298C5.03063 11.5198 4.26063 12.1398 3.75063 13.1498L2.02063 16.5998C1.40063 17.8498 1.46063 19.2998 2.19063 20.4798C2.92063 21.6598 4.19063 22.3698 5.58063 22.3698H18.3406C19.6806 22.3698 20.9306 21.6998 21.6706 20.5798C22.4306 19.4598 22.5506 18.0498 22.0206 16.8198Z" fill="#A6ADBB" ></path> <path d="M6.96984 8.38012C8.83657 8.38012 10.3498 6.86684 10.3498 5.00012C10.3498 3.13339 8.83657 1.62012 6.96984 1.62012C5.10312 1.62012 3.58984 3.13339 3.58984 5.00012C3.58984 6.86684 5.10312 8.38012 6.96984 8.38012Z" fill="#E5E7EB"></path> </g></svg>
|
||||
</a>
|
||||
|
||||
<a href="/community/create-post" class="text-white mr-2">
|
||||
<svg class="w-6 h-6 inline-block ml-1 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#A6ADBB"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.975 14.51a1.05 1.05 0 0 0 0-1.485 2.95 2.95 0 0 1 0-4.172l3.536-3.535a2.95 2.95 0 1 1 4.172 4.172l-1.093 1.092a1.05 1.05 0 0 0 1.485 1.485l1.093-1.092a5.05 5.05 0 0 0-7.142-7.142L9.49 7.368a5.05 5.05 0 0 0 0 7.142c.41.41 1.075.41 1.485 0zm2.05-5.02a1.05 1.05 0 0 0 0 1.485 2.95 2.95 0 0 1 0 4.172l-3.5 3.5a2.95 2.95 0 1 1-4.171-4.172l1.025-1.025a1.05 1.05 0 0 0-1.485-1.485L3.87 12.99a5.05 5.05 0 0 0 7.142 7.142l3.5-3.5a5.05 5.05 0 0 0 0-7.142 1.05 1.05 0 0 0-1.485 0z" fill="#A6ADBB"></path></g></svg>
|
||||
</a>
|
||||
|
||||
<!--
|
||||
<a href="#" class="text-gray-300">
|
||||
<svg class="w-6 h-6 inline-block" fill="#545B61" viewBox="-32 0 512 512" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M448 432V80c0-26.5-21.5-48-48-48H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48zM112 192c-8.84 0-16-7.16-16-16v-32c0-8.84 7.16-16 16-16h128c8.84 0 16 7.16 16 16v32c0 8.84-7.16 16-16 16H112zm0 96c-8.84 0-16-7.16-16-16v-32c0-8.84 7.16-16 16-16h224c8.84 0 16 7.16 16 16v32c0 8.84-7.16 16-16 16H112zm0 96c-8.84 0-16-7.16-16-16v-32c0-8.84 7.16-16 16-16h64c8.84 0 16 7.16 16 16v32c0 8.84-7.16 16-16 16h-64z"></path></g></svg>
|
||||
|
||||
</a>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -15,7 +15,7 @@
|
||||
<a
|
||||
href="/notifications"
|
||||
on:click={toggle}
|
||||
class="text-gray-300 sm:hover:text-white cursor-pointer sm:hover:bg-[#27272A] relative border p-2 rounded-lg border-gray-800 ml-3 -mr-1"
|
||||
class="text-gray-300 sm:hover:text-white cursor-pointer sm:hover:bg-[#27272A] relative border p-2 rounded-lg border-gray-600 ml-3 -mr-1"
|
||||
>
|
||||
<Bell class="h-[20px] w-[20px]" />
|
||||
|
||||
|
||||
@ -1,692 +0,0 @@
|
||||
<script lang = 'ts'>
|
||||
|
||||
import {getImageURL, formatDate} from '$lib/utils';
|
||||
import VideoPlayer from '$lib/components/VideoPlayer.svelte';
|
||||
import Share from '$lib/components/Share.svelte';
|
||||
import Downvote from '$lib/components/Downvote.svelte';
|
||||
import Upvote from '$lib/components/Upvote.svelte';
|
||||
|
||||
import {postVote, tagList, postIdDeleted} from '$lib/store';
|
||||
import toast from 'svelte-french-toast';
|
||||
|
||||
|
||||
export let posts;
|
||||
export let moderators;
|
||||
export let data;
|
||||
|
||||
let deletePostId = posts?.id
|
||||
|
||||
|
||||
|
||||
let isVideoClicked = {};
|
||||
|
||||
|
||||
function isModerator(userId) {
|
||||
return moderators?.some(moderator => userId === moderator?.user);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
let upvoteButtonClicked = {};
|
||||
let downvoteButtonClicked = {};
|
||||
let upvoteCounter = {};
|
||||
let downvoteCounter = {};
|
||||
let videoId = {};
|
||||
let userAlreadyVoted;
|
||||
|
||||
upvoteCounter[posts.id] = posts.upvote;
|
||||
downvoteCounter[posts.id] = posts.downvote;
|
||||
|
||||
|
||||
userAlreadyVoted = posts?.expand['alreadyVoted(post)']?.some(item => item?.user === data?.user?.id);
|
||||
|
||||
|
||||
if (userAlreadyVoted) {
|
||||
upvoteButtonClicked[posts.id] = posts?.expand['alreadyVoted(post)']?.find(item => item?.user === data?.user?.id)?.type === 'upvote';
|
||||
downvoteButtonClicked[posts.id] = posts?.expand['alreadyVoted(post)']?.find(item => item?.user === data?.user?.id)?.type === 'downvote';
|
||||
} else {
|
||||
upvoteButtonClicked[posts.id] = false;
|
||||
downvoteButtonClicked[posts.id] = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
|
||||
|
||||
await navigator.clipboard.writeText("https://stocknear.com/community/post/"+posts?.id);
|
||||
|
||||
toast.success('Link copied', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleDeletePost = async () => {
|
||||
|
||||
|
||||
const postData = {'postId': posts?.id,
|
||||
'userId': posts?.user,
|
||||
'path': 'delete-post',
|
||||
};
|
||||
|
||||
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())?.items;
|
||||
if (output === 'success')
|
||||
{
|
||||
$postIdDeleted = posts.id;
|
||||
|
||||
}
|
||||
|
||||
if (output === 'success') {
|
||||
toast.success('Post deleted', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
} else {
|
||||
toast.error('Something went wrong', {
|
||||
style: 'border-radius: 200px; background: #333; color: #fff;'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleUpvote = async (event) => {
|
||||
|
||||
event.preventDefault(); // prevent the default form submission behavior
|
||||
|
||||
const postId = event.target.postId.value;
|
||||
const postData = {
|
||||
'postId': postId,
|
||||
'userId': data?.user?.id,
|
||||
'path': 'upvote',
|
||||
};
|
||||
|
||||
upvoteButtonClicked[postId] = !upvoteButtonClicked[postId];
|
||||
|
||||
|
||||
if (upvoteButtonClicked[postId]) {
|
||||
if (downvoteButtonClicked[postId]) {
|
||||
upvoteCounter[postId] += 1;
|
||||
downvoteCounter[postId] -= 1;
|
||||
downvoteButtonClicked[postId] = false;
|
||||
} else {
|
||||
upvoteCounter[postId]++;
|
||||
}
|
||||
} else {
|
||||
upvoteCounter[postId]--;
|
||||
}
|
||||
|
||||
$postVote = {'id': postId, 'upvote': upvoteCounter[postId], 'downvote': downvoteCounter[postId], 'upvoteClicked': upvoteButtonClicked[postId], 'downvoteClicked': downvoteButtonClicked[postId]};
|
||||
|
||||
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 postId = event.target.postId.value;
|
||||
const postData = {
|
||||
'postId': postId,
|
||||
'userId': data?.user?.id,
|
||||
'path': 'downvote',
|
||||
};
|
||||
|
||||
downvoteButtonClicked[postId] = !downvoteButtonClicked[postId];
|
||||
|
||||
|
||||
if (downvoteButtonClicked[postId]) {
|
||||
if (upvoteButtonClicked[postId]) {
|
||||
downvoteCounter[postId] += 1;
|
||||
upvoteCounter[postId] -= 1;
|
||||
upvoteButtonClicked[postId] = false;
|
||||
} else {
|
||||
downvoteCounter[postId]++;
|
||||
}
|
||||
} else {
|
||||
downvoteCounter[postId]--;
|
||||
}
|
||||
|
||||
$postVote = {'id': postId, 'upvote': upvoteCounter[postId], 'downvote': downvoteCounter[postId], 'upvoteClicked': upvoteButtonClicked[postId], 'downvoteClicked': downvoteButtonClicked[postId]};
|
||||
|
||||
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
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function checkIfYoutubeVideo() {
|
||||
if (posts.postType === 'link') {
|
||||
const url = new URL(posts.link);
|
||||
if (url.hostname === "www.youtube.com") {
|
||||
const searchParams = url.searchParams;
|
||||
searchParams.delete('t'); // Remove the "t" parameter
|
||||
|
||||
const videoIdMatch = url.search.match(/v=([^&]+)/);
|
||||
if (videoIdMatch) {
|
||||
videoId[posts.id] = videoIdMatch[1];
|
||||
}
|
||||
} else {
|
||||
videoId[posts.id] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function clickVideo(event)
|
||||
{
|
||||
event.preventDefault();
|
||||
isVideoClicked[posts.id] = true;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
checkIfYoutubeVideo()
|
||||
|
||||
|
||||
$: {
|
||||
|
||||
if($postIdDeleted.length !== 0)
|
||||
{
|
||||
|
||||
upvoteCounter[posts.id] = posts.upvote;
|
||||
downvoteCounter[posts.id] = posts.downvote;
|
||||
|
||||
|
||||
userAlreadyVoted = posts?.expand['alreadyVoted(post)']?.some(item => item?.user === data?.user?.id);
|
||||
|
||||
|
||||
if (userAlreadyVoted) {
|
||||
upvoteButtonClicked[posts.id] = posts?.expand['alreadyVoted(post)']?.find(item => item?.user === data?.user?.id)?.type === 'upvote';
|
||||
downvoteButtonClicked[posts.id] = posts?.expand['alreadyVoted(post)']?.find(item => item?.user === data?.user?.id)?.type === 'downvote';
|
||||
} else {
|
||||
upvoteButtonClicked[posts.id] = false;
|
||||
downvoteButtonClicked[posts.id] = false;
|
||||
}
|
||||
|
||||
checkIfYoutubeVideo()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Posts -->
|
||||
|
||||
<div class="flex flex-row items-start w-full mt-5 relative">
|
||||
<div class="hidden lg:flex flex-col items-center absolute -top-2 -left-[70px]">
|
||||
<!--Start Upvote-->
|
||||
<form on:submit={handleUpvote}>
|
||||
<input type="hidden" name="postId" value={posts?.id}>
|
||||
{#if !data?.user}
|
||||
<div class="w-full">
|
||||
<label for="userLogin" class="cursor-pointer">
|
||||
<Upvote/>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
{:else}
|
||||
<button type="submit" class="w-full">
|
||||
{#if upvoteButtonClicked[posts?.id]}
|
||||
<Upvote state='active'/>
|
||||
{:else}
|
||||
<Upvote />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</form>
|
||||
<!--End Upvote-->
|
||||
<label class="text-center py-4 w-14 rounded-lg bg-[#27272A] border border-gray-700 text-[1rem] text-bold text-white">
|
||||
{upvoteCounter[posts?.id] - downvoteCounter[posts?.id] }
|
||||
</label>
|
||||
<!--Start Downvote-->
|
||||
<form on:submit={handleDownvote}>
|
||||
<input type="hidden" name="postId" value={posts?.id}>
|
||||
{#if !data?.user}
|
||||
<div class="w-full">
|
||||
<label for="userLogin" class="cursor-pointer">
|
||||
<Downvote />
|
||||
</label>
|
||||
</div>
|
||||
{:else}
|
||||
<button type="submit" class="w-full">
|
||||
{#if downvoteButtonClicked[posts?.id]}
|
||||
<Downvote state='active'/>
|
||||
{:else}
|
||||
<Downvote/>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</form>
|
||||
<!--End Downvote-->
|
||||
|
||||
</div>
|
||||
|
||||
<div class="w-full bg-[#141417] border-t border-b sm:border sm:hover:border-slate-600 border-gray-700 rounded-none sm:rounded-lg">
|
||||
<!-- List container -->
|
||||
<div class="flex flex-col">
|
||||
<!-- Item -->
|
||||
<div class="">
|
||||
<div class=" flex justify-between items-center mt-5 pb-2">
|
||||
|
||||
<div class="pl-2 flex flex-col items-center ">
|
||||
|
||||
{#if posts?.pinned}
|
||||
<div class="flex flex-row items-center mr-auto mb-4">
|
||||
<svg class="w-6 h-6 inline-block ml-2 mr-2" fill="#75D377" viewBox="0 0 1920 1920" xmlns="http://www.w3.org/2000/svg" stroke="#75D377"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M1154.976 0 988.342 166.52c-60.448 60.447-63.436 153.418-15.4 220.646L670.359 689.751c-4.022 4.022-6.55 8.964-9.079 13.79-147.212-61.022-328.671-34.246-444.626 81.709l-98.027 98.141 418.31 418.195-520.129 520.129c-22.41 22.409-22.41 58.724 0 81.248 11.262 11.147 25.972 16.778 40.682 16.778s29.42-5.63 40.567-16.778l520.128-520.129 418.195 418.31 98.142-98.142c75.962-75.847 117.793-176.862 117.793-284.313 0-56.195-12.067-110.208-33.787-160.198 2.758-1.839 5.861-2.988 8.275-5.516l303.963-303.964c29.19 21.145 63.896 33.097 100.67 33.097 46.083 0 89.293-17.928 121.93-50.565L1920 764.909 1154.976 0Z" fill-rule="evenodd"></path> </g></svg>
|
||||
<span class="text-slate-300 text-[0.8rem] sm:text-sm font-bold uppercase">
|
||||
Pinned by moderators
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
<a href={'/community/user/'+posts?.expand?.user?.id} class="flex flex-row items-center justify-start mr-auto">
|
||||
<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={posts?.expand?.user?.avatar
|
||||
? getImageURL(posts?.expand?.user?.collectionId, posts?.expand?.user?.id, posts?.expand?.user?.avatar)
|
||||
: `https://avatar.vercel.sh/${posts?.expand?.user?.username}`}
|
||||
alt="User avatar" />
|
||||
</label>
|
||||
<span class="text-white ml-2 inline-block text-sm">
|
||||
{posts?.expand?.user?.username}
|
||||
</span>
|
||||
{#if isModerator(posts?.user)}
|
||||
<svg class="inline-block ml-1 w-3.5 h-3.5" 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}
|
||||
|
||||
<span class="text-white font-bold ml-1 mr-1">
|
||||
·
|
||||
</span>
|
||||
<span class="text-white text-xs">
|
||||
{formatDate(posts?.created)} ago
|
||||
</span>
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--Start settings-->
|
||||
<div class="flex justify-end items-center dropdown dropdown-bottom dropdown-end mr-4">
|
||||
|
||||
|
||||
<a href={'/community/post/'+posts?.id} class="hidden lg:block ml-4 mr-4 flex justify-center items-center">
|
||||
<svg class="w-5 h-5 sm:w-4 sm:h-4 mt-1 inline-block mr-3" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64px" height="64px" viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve" fill="#D6D6DC"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" fill="#D6D6DC" d="M60,0H4C1.789,0,0,1.789,0,4v40c0,2.211,1.789,4,4,4h8v12 c0,1.617,0.973,3.078,2.469,3.695C14.965,63.902,15.484,64,16,64c1.039,0,2.062-0.406,2.828-1.172L33.656,48H60c2.211,0,4-1.789,4-4 V4C64,1.789,62.211,0,60,0z"></path> </g></svg>
|
||||
<span class="text-[1rem] sm:text-sm text-white font-semibold sm:font-medium">
|
||||
{#if posts?.expand['comments(post)']}
|
||||
{posts?.expand['comments(post)']?.length}
|
||||
{:else}
|
||||
0
|
||||
{/if}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<div class="flex-shrink-0 rounded-full hover:bg-white hover:bg-opacity-[0.05] w-8 h-8 relative flex items-center justify-center">
|
||||
<label tabindex="0" class="cursor-pointer flex-shrink-0">
|
||||
<svg class="m-auto w-4 h-4 sm:w-5 sm:h-5 " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||
<path fill="#cacaca" d="M156 128a28 28 0 1 1-28-28a28 28 0 0 1 28 28ZM48 100a28 28 0 1 0 28 28a28 28 0 0 0-28-28Zm160 0a28 28 0 1 0 28 28a28 28 0 0 0-28-28Z"/>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ul tabindex="0" class="border border-gray-700 dropdown-content menu bg-[#313131] rounded-box w-44 z-30">
|
||||
{#if data?.user?.id === posts?.user || isModerator(data?.user?.id)}
|
||||
<li>
|
||||
<label on:click={handleCopyLink} class="text-sm text-white cursor-pointer">
|
||||
<svg class="mr-auto w-5 h-5 inline-block" viewBox="0 -4 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#7B7B7B" stroke="#7B7B7B"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title>link_round [#7B7B7B]</title> <desc>Created with Sketch.</desc> <defs> </defs> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Dribbble-Light-Preview" transform="translate(-420.000000, -3283.000000)" fill="#7B7B7B"> <g id="icons" transform="translate(56.000000, 160.000000)"> <path d="M370.999774,3133 L369.999774,3133 C367.662774,3133 365.786774,3130.985 366.019774,3128.6 C366.221774,3126.522 368.089774,3125 370.177774,3125 L370.999774,3125 C371.551774,3125 371.999774,3124.552 371.999774,3124 C371.999774,3123.448 371.551774,3123 370.999774,3123 L370.251774,3123 C366.965774,3123 364.100774,3125.532 364.002774,3128.815 C363.900774,3132.213 366.624774,3135 369.999774,3135 L370.999774,3135 C371.551774,3135 371.999774,3134.552 371.999774,3134 C371.999774,3133.448 371.551774,3133 370.999774,3133 M377.747774,3123 L376.999774,3123 C376.447774,3123 375.999774,3123.448 375.999774,3124 C375.999774,3124.552 376.447774,3125 376.999774,3125 L377.821774,3125 C379.909774,3125 381.777774,3126.522 381.979774,3128.6 C382.212774,3130.985 380.336774,3133 377.999774,3133 L376.999774,3133 C376.447774,3133 375.999774,3133.448 375.999774,3134 C375.999774,3135.104 376.999774,3135 377.999774,3135 C381.374774,3135 384.098774,3132.213 383.996774,3128.815 C383.898774,3125.532 381.033774,3123 377.747774,3123 M368.999774,3128 L378.999774,3128 C379.551774,3128 379.999774,3128.448 379.999774,3129 C379.999774,3130.346 379.210774,3130 368.999774,3130 C368.447774,3130 367.999774,3129.552 367.999774,3129 C367.999774,3128.448 368.447774,3128 368.999774,3128" id="link_round-[#7B7B7B]"> </path> </g> </g> </g> </g></svg>
|
||||
Copy Link
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label for={deletePostId} class="text-sm text-[#E1341E] cursor-pointer">
|
||||
<svg class="mr-auto h-5 w-5 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#97372f" d="M7 21q-.825 0-1.413-.588T5 19V6q-.425 0-.713-.288T4 5q0-.425.288-.713T5 4h4q0-.425.288-.713T10 3h4q.425 0 .713.288T15 4h4q.425 0 .713.288T20 5q0 .425-.288.713T19 6v13q0 .825-.588 1.413T17 21H7ZM7 6v13h10V6H7Zm2 10q0 .425.288.713T10 17q.425 0 .713-.288T11 16V9q0-.425-.288-.713T10 8q-.425 0-.713.288T9 9v7Zm4 0q0 .425.288.713T14 17q.425 0 .713-.288T15 16V9q0-.425-.288-.713T14 8q-.425 0-.713.288T13 9v7ZM7 6v13V6Z"/></svg>
|
||||
Delete Post
|
||||
</label>
|
||||
</li>
|
||||
{:else}
|
||||
<li>
|
||||
<label on:click={handleCopyLink} class="text-sm text-white cursor-pointer">
|
||||
<svg class="mr-auto w-5 h-5 inline-block" viewBox="0 -4 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#7B7B7B" stroke="#7B7B7B"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title>link_round [#7B7B7B]</title> <desc>Created with Sketch.</desc> <defs> </defs> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Dribbble-Light-Preview" transform="translate(-420.000000, -3283.000000)" fill="#7B7B7B"> <g id="icons" transform="translate(56.000000, 160.000000)"> <path d="M370.999774,3133 L369.999774,3133 C367.662774,3133 365.786774,3130.985 366.019774,3128.6 C366.221774,3126.522 368.089774,3125 370.177774,3125 L370.999774,3125 C371.551774,3125 371.999774,3124.552 371.999774,3124 C371.999774,3123.448 371.551774,3123 370.999774,3123 L370.251774,3123 C366.965774,3123 364.100774,3125.532 364.002774,3128.815 C363.900774,3132.213 366.624774,3135 369.999774,3135 L370.999774,3135 C371.551774,3135 371.999774,3134.552 371.999774,3134 C371.999774,3133.448 371.551774,3133 370.999774,3133 M377.747774,3123 L376.999774,3123 C376.447774,3123 375.999774,3123.448 375.999774,3124 C375.999774,3124.552 376.447774,3125 376.999774,3125 L377.821774,3125 C379.909774,3125 381.777774,3126.522 381.979774,3128.6 C382.212774,3130.985 380.336774,3133 377.999774,3133 L376.999774,3133 C376.447774,3133 375.999774,3133.448 375.999774,3134 C375.999774,3135.104 376.999774,3135 377.999774,3135 C381.374774,3135 384.098774,3132.213 383.996774,3128.815 C383.898774,3125.532 381.033774,3123 377.747774,3123 M368.999774,3128 L378.999774,3128 C379.551774,3128 379.999774,3128.448 379.999774,3129 C379.999774,3130.346 379.210774,3130 368.999774,3130 C368.447774,3130 367.999774,3129.552 367.999774,3129 C367.999774,3128.448 368.447774,3128 368.999774,3128" id="link_round-[#7B7B7B]"> </path> </g> </g> </g> </g></svg>
|
||||
Copy Link
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label for="userLogin" class="text-sm cursor-pointer">
|
||||
<svg class="w-5 h-5 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="white" d="M12 2c5.5 0 10 4.5 10 10s-4.5 10-10 10S2 17.5 2 12S6.5 2 12 2m0 2c-1.9 0-3.6.6-4.9 1.7l11.2 11.2c1-1.4 1.7-3.1 1.7-4.9c0-4.4-3.6-8-8-8m4.9 14.3L5.7 7.1C4.6 8.4 4 10.1 4 12c0 4.4 3.6 8 8 8c1.9 0 3.6-.6 4.9-1.7Z"/></svg>
|
||||
Report
|
||||
</label>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
<!--End settings-->
|
||||
|
||||
</div>
|
||||
|
||||
<div >
|
||||
<div class="relative mt-2">
|
||||
<a href={"/community/post/"+posts?.id}>
|
||||
{#if posts?.postType === 'text'}
|
||||
<!--Start PostType Text-->
|
||||
<div class="flex flex-wrap md:flex-row container">
|
||||
<div class="cursor-pointer flex items-start">
|
||||
<div class="flex-grow w-full sm:w-3/4 max-w-2xl break-all text-white">
|
||||
|
||||
|
||||
<div class="mt-2 flex flex-wrap sm:hover:text-[#0099FF] text-start text-[1.1rem] sm:text-xl font-semibold mb-2 flex-shrink w-fit break-normal">
|
||||
{posts?.title}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="{posts?.description?.length > 400 ? 'darken-overlay' : ''} mt-3 text-sm sm:text-[1rem] whitespace-pre-line break-normal text-[#D7DADC]">
|
||||
{@html posts?.description}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End PostType Image-->
|
||||
{:else if posts?.postType==='image'}
|
||||
|
||||
|
||||
<!--Start PostType Image-->
|
||||
<div class="w-screen sm:w-full mt-2">
|
||||
<div class="ml-3 mr-3 flex flex-wrap sm:hover:text-[#0099FF] text-[1.1rem] sm:text-xl font-semibold text-white mb-5 relative z-10 whitenormal">
|
||||
{posts?.title}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="relative">
|
||||
{#if posts?.thumbnail && !posts?.thumbnail?.toLowerCase()?.includes('mp4')}
|
||||
<div class="absolute inset-0 bg-cover object-fill bg-center bg-[#000]"></div>
|
||||
|
||||
<!--<div class="absolute -inset-3 md:-inset-y-20 md:mt-10 bg-cover object-contain blur-[40px]" style="clip-path: polygon(0 0, 100% 0, 100% 90%, 0 90%); background-image: url('{getImageURL(posts.collectionId, posts.id, posts.thumbnail)}');"></div>-->
|
||||
<img src={getImageURL(posts?.collectionId, posts?.id, posts?.thumbnail)} alt = "post image" class="m-auto w-auto relative max-h-[520px] sm:max-h-[700px]" style="position: relative;" loading="lazy"/>
|
||||
{:else}
|
||||
<!--show videos here-->
|
||||
|
||||
|
||||
|
||||
<VideoPlayer src={getImageURL(posts?.collectionId, posts?.id, posts?.thumbnail)} />
|
||||
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!--End PostType Image-->
|
||||
{:else if posts?.postType === 'link'}
|
||||
<!--Start PostType Link -->
|
||||
|
||||
{#if videoId[posts?.id]?.length === 0}
|
||||
|
||||
|
||||
<div class="flex flex-col sm:flex-row items-center sm:items-start pr-7 pb-2">
|
||||
<div class="flex flex-col items-start w-full">
|
||||
<!-- Post Title -->
|
||||
<h2 class="pl-3 pt-2 text-white text-start sm:hover:text-[#0099FF] text-[1.1rem] sm:text-xl font-semibold w-full mb-1 pr-5 flex-shrink break-normal">
|
||||
{posts?.title}
|
||||
</h2>
|
||||
<!-- Post Description -->
|
||||
{#if posts?.description !== 'undefined'}
|
||||
<span class="block mt-2 text-start text-gray-100 text-[0.95rem] flex-shrink break-normal pl-3 w-11/12">
|
||||
{posts?.description?.length > 120 ? posts?.description.slice(0, 120) + "..." : posts?.description}
|
||||
</span>
|
||||
{/if}
|
||||
<a href={posts?.link} target="_blank" class="mt-2 text-sm pl-3 text-gray-400 font-semibold">
|
||||
{(new URL(posts?.link))?.hostname.replace('www.','')}
|
||||
<svg class="ml-1 w-4 h-4 inline-block -mt-0.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M21 9L21 3M21 3H15M21 3L13 11M10 5H7.8C6.11984 5 5.27976 5 4.63803 5.32698C4.07354 5.6146 3.6146 6.07354 3.32698 6.63803C3 7.27976 3 8.11984 3 9.8V16.2C3 17.8802 3 18.7202 3.32698 19.362C3.6146 19.9265 4.07354 20.3854 4.63803 20.673C5.27976 21 6.11984 21 7.8 21H14.2C15.8802 21 16.7202 21 17.362 20.673C17.9265 20.3854 18.3854 19.9265 18.673 19.362C19 18.7202 19 17.8802 19 16.2V14" stroke="#A6ADBB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{#if posts?.thumbnail}
|
||||
<div class="cursor-pointer w-full sm:w-56 sm:ml-auto mt-5 sm:mt-2 ml-6">
|
||||
<div class="flex-shrink-0 ">
|
||||
<a href={posts?.link} target="_blank" class="relative block w-full sm:w-56 h-full sm:max-h-[120px] overflow-hidden rounded-2xl">
|
||||
<img src={getImageURL(posts?.collectionId, posts?.id, posts?.thumbnail)} class="object-cover bg-contain bg-center bg-no-repeat w-screen sm:w-full max-h-[250px] sm:h-fit rounded-2xl border border-gray-700" alt="news image" loading="lazy">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
<div class="pt-2 w-full">
|
||||
|
||||
|
||||
<div class="ml-3 pr-7 flex flex-wrap sm:hover:text-[#0099FF] text-[1.1rem] sm:text-xl font-medium text-white mb-5 relative">
|
||||
{posts?.title}
|
||||
</div>
|
||||
<!--
|
||||
{#if !isVideoClicked[posts?.id] }
|
||||
|
||||
<div class="overflow-hidden bg-cover bg-no-repeat relative w-screen sm:w-full h-36 sm:h-[250px]">
|
||||
<label on:click={clickVideo} class="cursor-pointer transition duration-200 opacity-70 hover:opacity-90">
|
||||
<div class="relative transition duration-500 ease-in-out hover:scale-105">
|
||||
<img src="{getImageURL(posts?.collectionId, posts?.id, posts?.thumbnail)}" alt="image" class="m-auto w-full h-full object-cover" loading="lazy"/>
|
||||
<svg class="w-20 h-20 absolute top-1/3 left-1/2 transform -translate-x-1/2 -translate-y-1/2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path fill="none" d="M11 23a1 1 0 0 1-1-1V10a1 1 0 0 1 1.447-.894l12 6a1 1 0 0 1 0 1.788l-12 6A1.001 1.001 0 0 1 11 23Z"/>
|
||||
<path fill="#D6D6DC" d="M16 2a14 14 0 1 0 14 14A14 14 0 0 0 16 2Zm7.447 14.895l-12 6A1 1 0 0 1 10 22V10a1 1 0 0 1 1.447-.894l12 6a1 1 0 0 1 0 1.788Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
-->
|
||||
<iframe
|
||||
id="videoPlayer"
|
||||
class="w-full min-h-56 sm:min-h-96 sm:h-full max-h-[500px]"
|
||||
src={`https://www.youtube.com/embed/${videoId[posts.id]}`}
|
||||
frameborder="0"
|
||||
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
autoplay
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
<!--End PostType Link-->
|
||||
{/if}
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
<!--Start Post footer-->
|
||||
<div class="flex flex-col justify-start items-center mr-2 mt-8 ml-1.5 mb-4">
|
||||
<!--Start Tags-->
|
||||
<div class="flex flex-row items-center mr-auto ml-2">
|
||||
{#if posts?.tagline?.length !== 0}
|
||||
<label class="bg-[#D3D6DA] mr-3 text-xs h-fit font-medium text-[#171717] p-1 border rounded">
|
||||
<span class="p-0.5">{posts?.tagline}</span>
|
||||
</label>
|
||||
{/if}
|
||||
{#if posts?.tagTopic !== null}
|
||||
{#each $tagList as tag}
|
||||
{#each posts?.tagTopic as tagTopic}
|
||||
{#if tag?.name === tagTopic}
|
||||
<label class="bg-[#D3D6DA] mr-3 text-xs h-fit font-medium text-[#171717] p-1 border rounded">
|
||||
<span class="p-0.5">{tag?.name}</span>
|
||||
</label>
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<!--End Tags-->
|
||||
<!--Start-Up-Down-Vote-->
|
||||
<div class="lg:hidden flex flex-row items-center justify-start ml-14 mt-6 w-full ">
|
||||
<form on:submit={handleUpvote} class="-ml-4 lg:hidden mr-2">
|
||||
<input type="hidden" name="postId" value={posts?.id}>
|
||||
{#if !data?.user}
|
||||
<div class="w-full">
|
||||
<label for="userLogin" class="cursor-pointer">
|
||||
<Upvote/>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
{:else}
|
||||
<button type="submit" class="w-full">
|
||||
{#if upvoteButtonClicked[posts?.id]}
|
||||
<Upvote state='active'/>
|
||||
{:else}
|
||||
<Upvote />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</form>
|
||||
<span class="text-sm text-semibold lg:hidden">
|
||||
{upvoteCounter[posts?.id] - downvoteCounter[posts?.id] }
|
||||
</span>
|
||||
<form on:submit={handleDownvote} class="lg:hidden">
|
||||
<input type="hidden" name="postId" value={posts?.id}>
|
||||
{#if !data?.user}
|
||||
<div class="w-full ml-2 mr-2">
|
||||
<label for="userLogin" class="cursor-pointer">
|
||||
<Downvote />
|
||||
</label>
|
||||
</div>
|
||||
{:else}
|
||||
<button type="submit" class="w-full ml-2 mr-2">
|
||||
{#if downvoteButtonClicked[posts?.id]}
|
||||
<Downvote state='active'/>
|
||||
{:else}
|
||||
<Downvote/>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</form>
|
||||
<!--End-Up-Down-Vote-->
|
||||
<!--Start Comment Counter-->
|
||||
<a href={'/community/post/'+posts?.id} class="ml-6 lg:hidden flex justify-center items-center rounded-md">
|
||||
<svg class="w-4 h-4 mt-1 inline-block mr-2 lg:mr-3" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64px" height="64px" viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve" fill="#D6D6DC"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" fill="#D6D6DC" d="M60,0H4C1.789,0,0,1.789,0,4v40c0,2.211,1.789,4,4,4h8v12 c0,1.617,0.973,3.078,2.469,3.695C14.965,63.902,15.484,64,16,64c1.039,0,2.062-0.406,2.828-1.172L33.656,48H60c2.211,0,4-1.789,4-4 V4C64,1.789,62.211,0,60,0z"></path> </g></svg>
|
||||
<span class="ml-2 text-sm text-white font-semibold lg:font-medium">
|
||||
{#if posts?.expand['comments(post)']}
|
||||
{posts?.expand['comments(post)']?.length}
|
||||
{:else}
|
||||
0
|
||||
{/if}
|
||||
</span>
|
||||
</a>
|
||||
<!--End Comment Counter-->
|
||||
<!--Start Share Button-->
|
||||
<Share url ={'https://stocknear.com/community/post/'+posts?.id} />
|
||||
<!--End Share Button-->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!--End Post footer-->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--Start Delete Modal-->
|
||||
<input type="checkbox" id={deletePostId} class="modal-toggle" />
|
||||
|
||||
<dialog id={deletePostId} class="modal modal-bottom sm:modal-middle ">
|
||||
|
||||
|
||||
<label for={deletePostId} class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.5]"></label>
|
||||
|
||||
<div class="modal-box bg-[#27272A] p-10" >
|
||||
|
||||
|
||||
<h3 class="font-bold text-md sm:text-lg sm:mb-10">
|
||||
Are you sure you want to delete the post?
|
||||
</h3>
|
||||
|
||||
<div class="modal-action ">
|
||||
<label on:click={handleDeletePost} for={deletePostId} class="sm:ml-3 btn bg-[#FF3131] hover:bg-[#ff4343] text-white btn-md w-full rounded-lg text-white font-bold text-md">
|
||||
Proceed
|
||||
</label>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</dialog>
|
||||
<!--End Delete Modal-->
|
||||
|
||||
<style>
|
||||
.container {
|
||||
position: relative; /* Ensure relative positioning for the gradient overlay */
|
||||
overflow: hidden; /* To ensure the gradient does not overflow */
|
||||
max-height: 330px; /* Limit the container's height */
|
||||
width: 100%;
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
}
|
||||
|
||||
.darken-overlay::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50px; /* Adjust as needed for the gradient effect */
|
||||
background: linear-gradient(0deg, rgb(20, 20, 23, 1), rgb(20, 20, 23, 0)); /* Smooth gradient transition */
|
||||
pointer-events: none; /* Ensure it doesn't interfere with text interaction */
|
||||
}
|
||||
</style>
|
||||
@ -1,288 +0,0 @@
|
||||
<script lang="ts">
|
||||
import toast from "svelte-french-toast";
|
||||
import {
|
||||
commentAdded,
|
||||
commentUpdated,
|
||||
replyCommentClicked,
|
||||
editCommentClicked,
|
||||
} from "$lib/store";
|
||||
|
||||
export let data;
|
||||
export let postId = "";
|
||||
export let placeholder = "Leave a comment";
|
||||
export let commentId; //Important for replyComent when clicked Cancel it should close the TextEditor;
|
||||
export let inputValue = "";
|
||||
|
||||
let ref;
|
||||
let isLoaded = true;
|
||||
//let imageId = 'image-input';
|
||||
let expandField = commentId?.length !== 0 ? true : false;
|
||||
let imageInput = "";
|
||||
|
||||
/*
|
||||
const showPreview = (event) => {
|
||||
const target = event.target;
|
||||
const files = target.files;
|
||||
if (files.length > 0) {
|
||||
const src = URL.createObjectURL(files[0]);
|
||||
const preview = document.getElementById('image-preview');
|
||||
preview.src = src;
|
||||
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
async function handleComment(event) {
|
||||
if (isLoaded === true && inputValue?.length !== 0) {
|
||||
if ($editCommentClicked[commentId]) {
|
||||
toast.promise(
|
||||
updateComment(event),
|
||||
{
|
||||
loading: "Updating...",
|
||||
success: "Updated successfully!",
|
||||
error: "Something went wrong. Please try again...",
|
||||
},
|
||||
{
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
},
|
||||
);
|
||||
} else {
|
||||
toast.promise(
|
||||
createComment(event),
|
||||
{
|
||||
loading: "Creating...",
|
||||
success: "Commented successfully!",
|
||||
error: "Something went wrong. Please try again...",
|
||||
},
|
||||
{
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
isLoaded = true;
|
||||
}
|
||||
|
||||
const createComment = async (event) => {
|
||||
event.preventDefault(); // prevent the default form submission behavior
|
||||
let output;
|
||||
|
||||
try {
|
||||
if (isLoaded === true && inputValue?.length !== 0) {
|
||||
isLoaded = false;
|
||||
let formData = new FormData();
|
||||
|
||||
if ($replyCommentClicked[commentId]) {
|
||||
formData.append("reply", commentId);
|
||||
}
|
||||
formData.append("comment", inputValue);
|
||||
formData.append("post", postId);
|
||||
formData.append("image", imageInput.length !== 0 ? imageInput[0] : "");
|
||||
formData.append("user", data?.user?.id);
|
||||
|
||||
const response = await fetch("/api/create-comment", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
}); // make a POST request to the server with the FormData object
|
||||
|
||||
output = await response?.json();
|
||||
|
||||
if (output[0] === "success") {
|
||||
//Weird bug pocketbase says +1 for upvote but when i console.log(output[1]) i get upvote 0
|
||||
//Weird bug pocketbase does not expand correctly alreadyVoted when I create the comment but it works
|
||||
//when i retrieve all comments through the endpoint get-all-comments in fastify.
|
||||
//To circumvent this problem we manually check it again here:
|
||||
|
||||
let newComment = output[1];
|
||||
|
||||
if (newComment?.upvote === 0) {
|
||||
newComment.upvote = 1;
|
||||
}
|
||||
|
||||
if (!newComment?.expand["alreadyVoted(comment)"]) {
|
||||
newComment.expand["alreadyVoted(comment)"] = [];
|
||||
}
|
||||
|
||||
const alreadyVotedArray = newComment?.expand["alreadyVoted(comment)"];
|
||||
const hasAlreadyVoted = alreadyVotedArray?.some(
|
||||
(vote) => vote.type === "upvote" && vote.user === newComment?.user,
|
||||
);
|
||||
|
||||
if (!hasAlreadyVoted) {
|
||||
alreadyVotedArray.push({ type: "upvote", user: newComment?.user });
|
||||
}
|
||||
|
||||
$commentAdded = newComment;
|
||||
|
||||
inputValue = "";
|
||||
imageInput = "";
|
||||
|
||||
handleCancel();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
const updateComment = async (event) => {
|
||||
event.preventDefault(); // prevent the default form submission behavior
|
||||
let output;
|
||||
|
||||
try {
|
||||
if (isLoaded === true && inputValue?.length !== 0) {
|
||||
isLoaded = false;
|
||||
let formData = new FormData();
|
||||
formData.append("comment", inputValue?.trimStart());
|
||||
formData.append("commentId", commentId);
|
||||
|
||||
const response = await fetch("/api/update-comment", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
}); // make a POST request to the server with the FormData object
|
||||
|
||||
output = await response?.json();
|
||||
|
||||
if (output[0] === "success") {
|
||||
$commentUpdated = output[1];
|
||||
|
||||
inputValue = "";
|
||||
imageInput = "";
|
||||
handleCancel();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
function handleInput(event) {
|
||||
inputValue = event.target.value;
|
||||
/*
|
||||
const textarea = event.target;
|
||||
if($screenWidth >= 640)
|
||||
{
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, 10000) + 'px';
|
||||
}
|
||||
else {
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, 10000) + 'px';
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
if (commentId?.length !== 0) {
|
||||
$replyCommentClicked[commentId] = false;
|
||||
$editCommentClicked[commentId] = false;
|
||||
}
|
||||
|
||||
inputValue = "";
|
||||
imageInput = "";
|
||||
expandField = false;
|
||||
|
||||
const textarea = document.querySelector("textarea");
|
||||
textarea.style.height = "48px"; // 48px are h-12 in tailwindcss
|
||||
}
|
||||
|
||||
// Function to adjust textarea height
|
||||
function adjustHeight() {
|
||||
if (ref) {
|
||||
ref.style.height = "auto";
|
||||
ref.style.height = Math.min(ref.scrollHeight, 10000) + "px";
|
||||
}
|
||||
}
|
||||
|
||||
function handleImageInput(event) {
|
||||
imageInput = event.target.files;
|
||||
//console.log(imageInput)
|
||||
}
|
||||
|
||||
$: {
|
||||
if (expandField === true && typeof window !== "undefined") {
|
||||
ref?.focus();
|
||||
adjustHeight();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0" />
|
||||
|
||||
<div
|
||||
class="p-2 w-full max-w-xl mr-auto overflow-y-scroll {commentId?.length !== 0
|
||||
? '-ml-2'
|
||||
: ''}"
|
||||
>
|
||||
<textarea
|
||||
on:click={() => (expandField = true)}
|
||||
class="rounded-md text-sm {expandField
|
||||
? 'min-h-24 h-auto border-[#1C4090]'
|
||||
: 'h-12 border-gray-500'} overflow-hidden sm:hover:border-[#1C4090] sm:hover:ring-1 transition sm:ease-out placeholder-gray-300 w-full bg-[#27272A] text-white border border-1 ring-2 sm:ring-0 ring-[#1C4090]"
|
||||
{placeholder}
|
||||
value={inputValue}
|
||||
bind:this={ref}
|
||||
on:input={handleInput}
|
||||
/>
|
||||
|
||||
{#if expandField}
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-row items-center justify-end mt-4">
|
||||
<!--
|
||||
<div class="relative">
|
||||
<label for={imageId} class="{imageInput.length !== 0 ? 'hidden' : ''} inline-flex mr-auto items-center py-2.5 px-4 text-xs font-medium text-center text-white rounded-md sm:hover:bg-gray-300 cursor-pointer">
|
||||
<svg class="w-6 h-6 inline-block" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path opacity="0.4" d="M22.0206 16.8198L18.8906 9.49978C18.3206 8.15978 17.4706 7.39978 16.5006 7.34978C15.5406 7.29978 14.6106 7.96978 13.9006 9.24978L12.0006 12.6598C11.6006 13.3798 11.0306 13.8098 10.4106 13.8598C9.78063 13.9198 9.15063 13.5898 8.64063 12.9398L8.42063 12.6598C7.71063 11.7698 6.83063 11.3398 5.93063 11.4298C5.03063 11.5198 4.26063 12.1398 3.75063 13.1498L2.02063 16.5998C1.40063 17.8498 1.46063 19.2998 2.19063 20.4798C2.92063 21.6598 4.19063 22.3698 5.58063 22.3698H18.3406C19.6806 22.3698 20.9306 21.6998 21.6706 20.5798C22.4306 19.4598 22.5506 18.0498 22.0206 16.8198Z" fill="#A6ADBB" ></path> <path d="M6.96984 8.38012C8.83657 8.38012 10.3498 6.86684 10.3498 5.00012C10.3498 3.13339 8.83657 1.62012 6.96984 1.62012C5.10312 1.62012 3.58984 3.13339 3.58984 5.00012C3.58984 6.86684 5.10312 8.38012 6.96984 8.38012Z" fill="#E5E7EB"></path> </g></svg>
|
||||
|
||||
<input class="hidden rounded text-gray-200"
|
||||
type="file"
|
||||
id={imageId}
|
||||
name={imageId}
|
||||
on:input={handleImageInput}
|
||||
on:change={showPreview}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<label
|
||||
on:click={handleCancel}
|
||||
class="inline-flex justify-end items-center py-2.5 px-4 text-xs font-semibold text-white text-black rounded-md sm:hover:bg-red-700 cursor-pointer mr-3"
|
||||
>
|
||||
Cancel
|
||||
</label>
|
||||
{#if isLoaded}
|
||||
<label
|
||||
on:click={handleComment}
|
||||
class="inline-flex justify-end items-center bg-[#fff] sm:hover:bg-gray-300 duration-100 transition {inputValue.length !==
|
||||
0
|
||||
? 'opacity-100 cursor-pointer'
|
||||
: 'opacity-60'} py-2.5 px-4 text-xs font-semibold text-center text-black rounded-md focus:ring-purple-300"
|
||||
>
|
||||
Post
|
||||
</label>
|
||||
{:else}
|
||||
<label
|
||||
class="cursor-not-allowed inline-flex justify-end items-center bg-[#fff] bg-opacity-40 py-2.5 px-4 text-xs font-medium text-center text-white rounded-md focus:ring-purple-300"
|
||||
>
|
||||
Post
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if imageInput.length !== 0}
|
||||
<div class="relative mr-auto -mt-8">
|
||||
<label
|
||||
on:click={() => (imageInput = "")}
|
||||
class="text-black w-7 h-7 text-center m-auto flex justify-center items-center rounded-full bg-white absolute top-0 -right-2"
|
||||
>✕</label
|
||||
>
|
||||
<img
|
||||
class="object-contain max-w-full max-h-[150px] mx-auto"
|
||||
alt="Image preview"
|
||||
id="image-preview"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@ -1,121 +0,0 @@
|
||||
<script lang = 'ts'>
|
||||
|
||||
import {getImageURL} from '$lib/utils';
|
||||
|
||||
import { goto} from '$app/navigation';
|
||||
|
||||
export let data;
|
||||
export let rank;
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class=" {rank === 1 ? 'h-48 sm:h-60 mr-2 sm:mr-10 light-box-1' : rank === 2 ? 'h-40 sm:h-48 ml-2 mr-2 sm:mr-10 light-box-2' : 'h-36 sm:h-40 mr-2 light-box-3'} w-full sm:w-48 border border-slate-600 hover:ring-[1px] hover:ring-purple-600 ease-in-out delay-50 bg-gray-400 rounded-[25px] sm:rounded-xl bg-opacity-[0.3] mt-auto relative">
|
||||
{#if rank ===1}
|
||||
<svg class="w-9 h-9 sm:w-12 sm:h-12 absolute -top-1 right-0" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve" fill="#ffffff"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g> <polygon style="fill:#4675CF;" points="186.205,408.076 90.454,503.826 66.108,437.718 0,413.372 82.217,331.155 122.966,290.406 211.543,378.982 "></polygon> <polygon style="fill:#4675CF;" points="405.487,306.86 512,413.372 445.891,437.718 421.545,503.826 298.975,381.256 "></polygon> </g> <g> <polygon style="fill:#4E8FE3;" points="146.568,314.077 34.53,426.115 66.038,437.718 78.385,471.244 191.06,358.569 "></polygon> <polygon style="fill:#4E8FE3;" points="477.501,426.233 377.778,326.51 325.388,363.103 433.85,471.566 446.315,437.718 "></polygon> </g> <g> <path style="opacity:0.23;fill:#3F489B;enable-background:new ;" d="M405.487,306.876l-106.513,74.395l45.268,45.268 c40.657-16.832,75.604-44.65,101.152-79.756L405.487,306.876z"></path> <path style="opacity:0.23;fill:#3F489B;enable-background:new ;" d="M82.217,331.171l-18.325,18.325 c26.069,34.779,61.476,62.152,102.502,78.406l19.81-19.81l25.338-29.094l-88.577-88.577L82.217,331.171z"></path> </g> <path style="fill:#CCA400;" d="M275.43,11.923L275.43,11.923c27.832-9.859,58.816,0.208,75.536,24.544l0,0 c8.465,12.321,20.907,21.36,35.241,25.604l0,0c28.312,8.382,47.461,34.738,46.684,64.255l0,0 c-0.393,14.943,4.359,29.569,13.461,41.428l0,0c17.978,23.422,17.978,56.002,0,79.424l0,0 c-9.102,11.858-13.854,26.484-13.461,41.428l0,0c0.777,29.517-18.372,55.873-46.684,64.255l0,0 c-14.334,4.244-26.775,13.283-35.241,25.604l0,0c-16.721,24.336-47.705,34.403-75.536,24.544l0,0 c-14.091-4.992-29.469-4.992-43.56,0l0,0c-27.832,9.859-58.816-0.208-75.536-24.544l0,0c-8.466-12.321-20.907-21.36-35.241-25.604 l0,0c-28.312-8.382-47.461-34.738-46.684-64.255l0,0c0.393-14.943-4.359-29.569-13.461-41.428l0,0 c-17.978-23.422-17.978-56.002,0-79.424l0,0c9.102-11.858,13.854-26.484,13.461-41.428l0,0 c-0.777-29.517,18.372-55.873,46.684-64.255l0,0c14.334-4.244,26.775-13.283,35.241-25.604l0,0 c16.721-24.336,47.705-34.403,75.536-24.544l0,0C245.961,16.915,261.339,16.915,275.43,11.923z"></path> <circle style="fill:#EEBF00;" cx="253.646" cy="207.466" r="154.651"></circle> <polygon style="fill:#FFEB99;" points="237.07,141.954 212.717,141.954 212.717,179.386 237.07,179.386 237.07,283.564 274.502,283.564 274.502,179.386 274.502,141.954 "></polygon> <g> <path style="fill:#CCA400;" d="M188.354,234.018c17.682,17.682,14.244,49.786,14.244,49.786s-32.105,3.437-49.786-14.244 c-17.682-17.682-14.244-49.786-14.244-49.786S170.673,216.336,188.354,234.018z"></path> <path style="fill:#CCA400;" d="M355.151,269.559c-17.682,17.682-49.786,14.244-49.786,14.244s-3.437-32.105,14.244-49.786 s49.786-14.244,49.786-14.244S372.833,251.878,355.151,269.559z"></path> </g> <path style="opacity:0.14;fill:#56361D;enable-background:new ;" d="M446.424,167.754L446.424,167.754 c-9.102-11.858-13.854-26.484-13.461-41.428l0,0c0.777-29.517-18.372-55.873-46.684-64.255l0,0 c-13.093-3.876-24.595-11.763-32.942-22.486c19.976,31.429,31.553,68.72,31.553,108.717c0,112.091-90.868,202.957-202.957,202.957 c-34.771,0-67.497-8.75-96.104-24.16c8.255,12.085,20.54,21.38,35.336,25.762l0,0c14.334,4.244,26.775,13.283,35.241,25.604l0,0 c16.721,24.336,47.705,34.403,75.536,24.544l0,0c14.091-4.991,29.469-4.991,43.56,0l0,0c27.832,9.859,58.816-0.208,75.536-24.544 l0,0c8.465-12.321,20.907-21.36,35.241-25.604l0,0c28.312-8.382,47.461-34.739,46.684-64.255l0,0 c-0.393-14.943,4.359-29.569,13.461-41.428l0,0C464.401,223.754,464.401,191.176,446.424,167.754z"></path> </g></svg>
|
||||
{:else if rank === 2}
|
||||
<svg class="w-9 h-9 sm:w-12 sm:h-12 absolute -top-1 right-0" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve" fill="#A87429" stroke="#A87429"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <polygon style="fill:#4675CF;" points="189.93,385.281 189.93,512 256.001,481.496 322.071,512 322.071,388.673 "></polygon> <polygon style="fill:#4E8FE3;" points="221.58,386.094 221.58,497.388 256.001,481.496 290.42,497.388 290.42,387.86 "></polygon> <path style="opacity:0.22;fill:#3F489B;enable-background:new ;" d="M189.93,434.134c19.725,7.879,41.246,12.22,63.785,12.22 c24.292,0,47.403-5.045,68.356-14.127v-43.552l-132.141-3.391V434.134z"></path> <path style="fill:#ACAAB1;" d="M278.498,3.874L278.498,3.874c28.749-10.185,60.756,0.215,78.028,25.353l0,0 c8.745,12.728,21.597,22.064,36.403,26.449l0,0c29.246,8.659,49.026,35.884,48.224,66.374l0,0 c-0.406,15.436,4.503,30.544,13.905,42.795l0,0c18.571,24.195,18.571,57.849,0,82.043l0,0 c-9.403,12.249-14.311,27.357-13.905,42.795l0,0c0.802,30.49-18.978,57.716-48.224,66.374l0,0 c-14.807,4.384-27.658,13.721-36.403,26.449l0,0c-17.273,25.139-49.278,35.538-78.028,25.353l0,0 c-14.555-5.157-30.441-5.157-44.997,0l0,0c-28.749,10.185-60.756-0.215-78.028-25.353l0,0 c-8.745-12.728-21.597-22.064-36.403-26.449l0,0c-29.246-8.659-49.026-35.884-48.224-66.374l0,0 c0.406-15.436-4.503-30.544-13.905-42.795l0,0c-18.571-24.195-18.571-57.849,0-82.043l0,0c9.403-12.249,14.311-27.357,13.905-42.795 l0,0c-0.802-30.49,18.978-57.716,48.224-66.374l0,0c14.807-4.384,27.658-13.721,36.403-26.449l0,0 c17.273-25.14,49.278-35.539,78.028-25.353l0,0C248.057,9.03,263.943,9.03,278.498,3.874z"></path> <circle style="fill:#D7D5D9;" cx="255.997" cy="205.861" r="159.751"></circle> <path style="fill:#C59F3A;" d="M297.495,160.142c0-12.123-9.828-21.949-21.949-21.949h-16.717h-48.895v38.666h38.574 c4.904,0,8.227,4.995,6.331,9.518l-41.116,98.095h32.702h5.965h22.553h22.552v-38.666h-22.552h-6.346l25.735-61.399 c2.088-4.983,3.164-10.331,3.164-15.733V160.142z"></path> <g> <path style="fill:#ACAAB1;" d="M339.764,299.886c-2.039,0-4.08-0.778-5.636-2.335c-3.113-3.113-3.113-8.159,0-11.271 c44.342-44.341,44.342-116.489,0-160.83c-3.113-3.113-3.113-8.159,0-11.271c3.114-3.113,8.159-3.113,11.272,0 c24.49,24.49,37.977,57.052,37.977,91.686s-13.486,67.196-37.977,91.686C343.844,299.108,341.804,299.886,339.764,299.886z"></path> <path style="fill:#ACAAB1;" d="M167.664,299.886c-2.039,0-4.08-0.778-5.635-2.335c-24.49-24.49-37.978-57.052-37.978-91.686 s13.487-67.196,37.978-91.686c3.113-3.113,8.159-3.113,11.271,0c3.113,3.113,3.113,8.159,0,11.271 c-44.341,44.341-44.341,116.489,0,160.83c3.113,3.113,3.113,8.159,0,11.271C171.744,299.108,169.703,299.886,167.664,299.886z"></path> </g> <path style="opacity:0.14;fill:#56361D;enable-background:new ;" d="M454.826,164.844L454.826,164.844 c-9.402-12.249-14.311-27.357-13.905-42.795l0,0c0.802-30.49-18.978-57.716-48.224-66.374l0,0 c-13.525-4.004-25.406-12.151-34.029-23.228c20.635,32.466,32.594,70.986,32.594,112.303c0,115.788-93.865,209.651-209.651,209.651 c-35.918,0-69.723-9.039-99.274-24.957c8.527,12.483,21.217,22.085,36.502,26.611l0,0c14.807,4.384,27.658,13.721,36.403,26.449l0,0 c17.273,25.139,49.278,35.538,78.028,25.353l0,0c14.555-5.156,30.441-5.156,44.997,0l0,0c28.749,10.185,60.756-0.215,78.028-25.353 l0,0c8.745-12.728,21.597-22.064,36.403-26.449l0,0c29.246-8.659,49.026-35.884,48.224-66.374l0,0 c-0.406-15.436,4.503-30.544,13.905-42.795l0,0C473.396,222.691,473.396,189.039,454.826,164.844z"></path> </g></svg>
|
||||
{:else if rank === 3}
|
||||
<svg class="w-9 h-9 sm:w-12 sm:h-12 absolute -top-1 right-0" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve" fill="#ffffff"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <polygon style="fill:#4675CF;" points="189.249,385.281 189.249,512 255.319,481.496 321.39,512 321.39,381.076 "></polygon> <g> <polygon style="fill:#4E8FE3;" points="220.895,386.094 220.895,497.388 255.316,481.496 289.737,497.388 289.737,387.86 "></polygon> <polygon style="fill:#4E8FE3;" points="220.895,386.094 220.895,497.388 255.316,481.496 289.737,497.388 289.737,387.86 "></polygon> </g> <path style="opacity:0.22;fill:#3F489B;enable-background:new ;" d="M189.249,438.754c19.725,7.879,41.246,12.22,63.785,12.22 c24.292,0,47.403-5.045,68.356-14.127v-43.552l-132.141-3.391L189.249,438.754L189.249,438.754z"></path> <path style="fill:#A87429;" d="M277.817,3.874L277.817,3.874c28.749-10.185,60.756,0.215,78.028,25.353l0,0 c8.745,12.728,21.597,22.064,36.403,26.449l0,0c29.246,8.659,49.026,35.884,48.224,66.374l0,0 c-0.406,15.436,4.503,30.544,13.905,42.795l0,0c18.571,24.195,18.571,57.849,0,82.043l0,0 c-9.402,12.249-14.311,27.357-13.905,42.795l0,0c0.802,30.49-18.978,57.716-48.224,66.374l0,0 c-14.807,4.384-27.658,13.721-36.403,26.449l0,0c-17.273,25.139-49.278,35.538-78.028,25.353l0,0 c-14.555-5.157-30.441-5.157-44.997,0l0,0c-28.749,10.185-60.756-0.215-78.028-25.353l0,0 c-8.745-12.728-21.597-22.064-36.403-26.449l0,0c-29.246-8.659-49.026-35.884-48.224-66.374l0,0 c0.406-15.436-4.503-30.544-13.905-42.795l0,0c-18.571-24.195-18.571-57.849,0-82.043l0,0c9.403-12.249,14.311-27.357,13.905-42.795 l0,0c-0.802-30.49,18.978-57.716,48.224-66.374l0,0c14.807-4.384,27.658-13.721,36.403-26.449l0,0 c17.273-25.14,49.278-35.539,78.028-25.353l0,0C247.376,9.03,263.262,9.03,277.817,3.874z"></path> <circle style="fill:#E8A554;" cx="255.317" cy="205.861" r="159.751"></circle> <g> <polygon style="fill:#A87429;" points="228.117,198.209 228.117,224.456 247.347,224.456 247.347,245.806 215.715,245.806 215.715,284.474 247.347,284.474 280.014,284.474 286.014,284.474 286.014,176.859 286.014,138.193 247.347,138.193 213.497,138.193 213.497,176.859 247.347,176.859 247.347,198.209 "></polygon> <path style="fill:#A87429;" d="M341.366,299.886c-2.039,0-4.08-0.778-5.636-2.335c-3.113-3.113-3.113-8.159,0-11.271 c44.342-44.341,44.342-116.489,0-160.83c-3.113-3.113-3.113-8.159,0-11.271c3.114-3.113,8.159-3.113,11.272,0 c24.49,24.49,37.977,57.052,37.977,91.686s-13.486,67.196-37.977,91.686C345.446,299.108,343.405,299.886,341.366,299.886z"></path> <path style="fill:#A87429;" d="M169.265,299.886c-2.039,0-4.08-0.778-5.636-2.335c-24.49-24.49-37.977-57.052-37.977-91.686 s13.486-67.196,37.977-91.686c3.114-3.113,8.159-3.113,11.272,0s3.113,8.159,0,11.271c-44.342,44.341-44.342,116.489,0,160.83 c3.113,3.113,3.113,8.159,0,11.271C173.345,299.108,171.305,299.886,169.265,299.886z"></path> </g> <path style="opacity:0.14;fill:#56361D;enable-background:new ;" d="M455.739,164.844L455.739,164.844 c-9.402-12.249-14.311-27.357-13.905-42.795l0,0c0.802-30.49-18.978-57.716-48.224-66.374l0,0 c-13.525-4.004-25.406-12.151-34.029-23.228c20.635,32.466,32.594,70.986,32.594,112.303c0,115.788-93.865,209.651-209.651,209.651 c-35.918,0-69.723-9.039-99.274-24.957c8.527,12.483,21.217,22.085,36.502,26.611l0,0c14.807,4.384,27.658,13.721,36.403,26.449l0,0 c17.273,25.139,49.278,35.538,78.028,25.353l0,0c14.555-5.156,30.441-5.156,44.997,0l0,0c28.749,10.185,60.756-0.215,78.028-25.353 l0,0c8.745-12.728,21.597-22.064,36.403-26.449l0,0c29.246-8.659,49.026-35.884,48.224-66.374l0,0 c-0.406-15.436,4.503-30.544,13.905-42.795l0,0C474.31,222.691,474.31,189.039,455.739,164.844z"></path> </g></svg>
|
||||
{/if}
|
||||
<label on:click={() => goto("/community/user/"+data?.user)} class="cursor-pointer">
|
||||
<div class="flex flex-col items-center mb-3 ">
|
||||
<span class="text-white mt-3 {rank === 1 ? 'text-xs sm:text-xl' : rank === 2 ? 'text-xs sm:text-md' : 'text-xs sm:text-md'} ">
|
||||
{data?.expand?.user?.username}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded-full m-auto {rank === 1 ? 'h-20 w-20' : rank === 2 ? 'h-16 w-16' : 'w-12 h-12'} relative">
|
||||
<img style="clip-path: circle(50%);"
|
||||
class="rounded-full {rank === 1 ? 'w-16 h-16 sm:w-20 sm:h-20' : rank === 2 ? 'w-14 h-14 sm:w-16 sm:h-16' : 'w-11 h-11 sm:w-12 sm:h-12'} absolute inset-1/2 transform -translate-x-1/2 -translate-y-1/2"
|
||||
src={data?.expand?.user?.avatar
|
||||
? getImageURL(data?.expand?.user?.collectionId, data?.expand?.user?.id, data?.expand?.user?.avatar)
|
||||
: `https://avatar.vercel.sh/${data?.expand?.user?.username}`}
|
||||
alt="User avatar" loading="lazy"/>
|
||||
|
||||
</div>
|
||||
|
||||
</label>
|
||||
|
||||
<div class="flex flex-col items-center {rank === 1 ? 'text-xs sm:text-sm mt-6 sm:mt-10' : rank === 2 ? 'mt-2 sm:mt-6 text-xs' : 'mt-2.5 text-xs'}">
|
||||
|
||||
${new Intl.NumberFormat("en", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(data?.accountValue)}
|
||||
|
||||
<div class="flex flex-row items-center mt-1">
|
||||
|
||||
{#if data?.overallReturn > 0}
|
||||
<svg class="w-5 h-5 -mr-0.5 mt-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g id="evaArrowUpFill0"><g id="evaArrowUpFill1"><path id="evaArrowUpFill2" fill="#00FC50" d="M16.21 16H7.79a1.76 1.76 0 0 1-1.59-1a2.1 2.1 0 0 1 .26-2.21l4.21-5.1a1.76 1.76 0 0 1 2.66 0l4.21 5.1A2.1 2.1 0 0 1 17.8 15a1.76 1.76 0 0 1-1.59 1Z"/></g></g></svg>
|
||||
|
||||
<span class="text-[#00FC50] text-md">
|
||||
+{data?.overallReturn.toFixed(2)}%
|
||||
</span>
|
||||
{:else if data?.overallReturn < 0}
|
||||
<svg class="w-5 h-5 -mr-0.5 mt-0.5 rotate-180" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g id="evaArrowUpFill0"><g id="evaArrowUpFill1"><path id="evaArrowUpFill2" fill="#FF2F1F" d="M16.21 16H7.79a1.76 1.76 0 0 1-1.59-1a2.1 2.1 0 0 1 .26-2.21l4.21-5.1a1.76 1.76 0 0 1 2.66 0l4.21 5.1A2.1 2.1 0 0 1 17.8 15a1.76 1.76 0 0 1-1.59 1Z"/></g></g></svg>
|
||||
<span class="text-[#FF2F1F] text-md">
|
||||
{data?.overallReturn?.toFixed(2)}%
|
||||
</span>
|
||||
{:else}
|
||||
<svg class="w-2.5 h-2.5 inline-block mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path fill="#fff" d="M3 19h18a1.002 1.002 0 0 0 .823-1.569l-9-13c-.373-.539-1.271-.539-1.645 0l-9 13A.999.999 0 0 0 3 19z"/></g></svg>
|
||||
<span class="text-white text-md">
|
||||
{data?.overallReturn.toFixed(2)}%
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
|
||||
.light-box-1 {
|
||||
--border-size: 2px;
|
||||
--border-angle: 0turn;
|
||||
background-image: conic-gradient(from var(--border-angle), #213, #112 50%, #213), conic-gradient(from var(--border-angle), transparent 20%, #08f, #f03);
|
||||
background-size: calc(100% - (var(--border-size) * 2)) calc(100% - (var(--border-size) * 2)), cover;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
animation: bg-spin 3s linear infinite;
|
||||
@keyframes bg-spin {
|
||||
to {
|
||||
--border-angle: 1turn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@property --border-angle {
|
||||
syntax: "<angle>";
|
||||
inherits: true;
|
||||
initial-value: 0turn;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.light-box-2 {
|
||||
--border-size: 1px;
|
||||
--border-angle: 0turn;
|
||||
background-image: conic-gradient(from var(--border-angle), #213, #112 50%, #213), conic-gradient(from var(--border-angle), transparent 20%, #08f, #f03);
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
|
||||
.light-box-3 {
|
||||
--border-size: 1px;
|
||||
--border-angle: 0turn;
|
||||
background-image: conic-gradient(from var(--border-angle), #213, #112 50%, #213), conic-gradient(from var(--border-angle), transparent 20%, #08f, #f03);
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@ -31,10 +31,7 @@
|
||||
etfTicker,
|
||||
loginData,
|
||||
numberOfUnreadNotification,
|
||||
cachedPosts,
|
||||
currentPagePosition,
|
||||
clientSideCache,
|
||||
twitchStatus,
|
||||
} from "$lib/store";
|
||||
|
||||
import { Button } from "$lib/components/shadcn/button/index.ts";
|
||||
@ -53,7 +50,6 @@
|
||||
import Layers from "lucide-svelte/icons/layers";
|
||||
import Boxes from "lucide-svelte/icons/boxes";
|
||||
import Newspaper from "lucide-svelte/icons/newspaper";
|
||||
import MessageCircle from "lucide-svelte/icons/message-circle";
|
||||
import AudioLine from "lucide-svelte/icons/audio-lines";
|
||||
import Gem from "lucide-svelte/icons/gem";
|
||||
|
||||
@ -108,9 +104,7 @@
|
||||
hideHeader = true; // Hide the header for other routes under "/etf/"
|
||||
} else {
|
||||
// Specify conditions for other routes where you want to hide the header
|
||||
hideHeader =
|
||||
currentPath.startsWith("/community/post") ||
|
||||
currentPath.startsWith("/stocks/");
|
||||
hideHeader = currentPath.startsWith("/stocks/");
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,10 +114,7 @@
|
||||
$page.url.pathname.startsWith("/login") ||
|
||||
$page.url.pathname.startsWith("/etf") ||
|
||||
$page.url.pathname.startsWith("/portfolio") ||
|
||||
$page.url.pathname.startsWith("/hedge-funds") ||
|
||||
$page.url.pathname.startsWith("/community");
|
||||
|
||||
//$: hideSidebar = $page.url.pathname.startsWith('/contact') || $page.url.pathname.startsWith('/imprint') || $page.url.pathname.startsWith('/privacy-policy') || $page.url.pathname.startsWith('/terms-of-use') || $page.url.pathname.startsWith('/about') || $page.url.pathname.startsWith('/community/create-post') || $page.url.pathname.startsWith('/login') || $page.url.pathname.startsWith('/register')
|
||||
$page.url.pathname.startsWith("/hedge-funds");
|
||||
|
||||
let hasUnreadElement = false;
|
||||
let notificationList = [];
|
||||
@ -214,18 +205,6 @@ const handleTwitchMessage = (event) => {
|
||||
NProgress.done();
|
||||
});
|
||||
|
||||
//Delete cached Posts when going to other directories
|
||||
$: {
|
||||
if (
|
||||
$page.url.pathname === "/community" ||
|
||||
$page.url.pathname.startsWith("/community/post")
|
||||
) {
|
||||
} else {
|
||||
$cachedPosts = [];
|
||||
$currentPagePosition = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($page.url.pathname) {
|
||||
$loginData = data?.user;
|
||||
@ -783,7 +762,9 @@ const handleTwitchMessage = (event) => {
|
||||
>
|
||||
<Gem class="h-5.5 w-5.5" />
|
||||
</div>
|
||||
<span class="ml-3 text-white text-[1rem]">Pricing</span>
|
||||
<span class="ml-3 text-white text-[1rem]"
|
||||
>Stock Analysis Pro</span
|
||||
>
|
||||
</div>
|
||||
</a>
|
||||
</Button>
|
||||
@ -852,27 +833,23 @@ const handleTwitchMessage = (event) => {
|
||||
<DropdownMenu.Trigger asChild let:builder>
|
||||
<Button
|
||||
size="icon"
|
||||
class="overflow-hidden rounded-full"
|
||||
class="overflow-hidden rounded-lg bg-[#09090B] border border-gray-600 w-10 h-10"
|
||||
builders={[builder]}
|
||||
>
|
||||
<img
|
||||
src={data?.user?.avatar
|
||||
? getImageURL(
|
||||
data?.user.collectionId,
|
||||
data?.user.id,
|
||||
data?.user.avatar,
|
||||
)
|
||||
: defaultAvatar}
|
||||
width={36}
|
||||
height={36}
|
||||
alt="Avatar"
|
||||
class="overflow-hidden rounded-full"
|
||||
/>
|
||||
<svg
|
||||
class="h-[28px] w-[28px] overflow-hidden rounded-full text-gray-300"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M12 4a4 4 0 0 1 4 4a4 4 0 0 1-4 4a4 4 0 0 1-4-4a4 4 0 0 1 4-4m0 10c4.42 0 8 1.79 8 4v2H4v-2c0-2.21 3.58-4 8-4"
|
||||
/></svg
|
||||
>
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item class="sm:hover:bg-[#27272A]">
|
||||
<a href="/community/profile"> My Account </a>
|
||||
<a href="/profile"> My Account </a>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item class="sm:hover:bg-[#27272A]">
|
||||
@ -1185,7 +1162,7 @@ const handleTwitchMessage = (event) => {
|
||||
>
|
||||
<Gem class="h-5.5 w-5.5" />
|
||||
</div>
|
||||
<span class="ml-3 text-white">Pricing</span>
|
||||
<span class="ml-3 text-white">Stock Analysis Pro</span>
|
||||
</a>
|
||||
</nav>
|
||||
{#if !data?.user || data?.user?.tier === "Free" || data?.user?.freeTrial === true}
|
||||
|
||||
@ -1,204 +0,0 @@
|
||||
import { error, fail, redirect } from "@sveltejs/kit";
|
||||
import { validateData } from "$lib/utils";
|
||||
import { loginUserSchema, registerUserSchema } from "$lib/schemas";
|
||||
|
||||
export const load = async ({ locals, setHeaders }) => {
|
||||
const { pb } = locals;
|
||||
|
||||
const getDiscordWidget = async () => {
|
||||
// make the POST request to the endpoint
|
||||
const response = await fetch(
|
||||
"https://discord.com/api/guilds/1165618982133436436/widget.json",
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const output = await response.json();
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
const getCommunityStats = async () => {
|
||||
let output;
|
||||
let totalUsers = 0;
|
||||
let totalPosts = 0;
|
||||
let totalComments = 0;
|
||||
|
||||
try {
|
||||
totalUsers = (await pb.collection("users").getList(1, 1))?.totalItems;
|
||||
totalPosts = (await pb.collection("posts").getList(1, 1))?.totalItems;
|
||||
totalComments = (await pb.collection("comments").getList(1, 1))
|
||||
?.totalItems;
|
||||
|
||||
output = { totalUsers, totalPosts, totalComments };
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
output = { totalUsers, totalPosts, totalComments };
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
const getModerators = async () => {
|
||||
let output;
|
||||
try {
|
||||
output = await pb.collection("moderators").getFullList({
|
||||
expand: "user",
|
||||
});
|
||||
} catch (e) {
|
||||
output = [];
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
setHeaders({ "cache-control": "public, max-age=3000" });
|
||||
// Make sure to return a promise
|
||||
return {
|
||||
getDiscordWidget: await getDiscordWidget(),
|
||||
getCommunityStats: await getCommunityStats(),
|
||||
getModerators: await getModerators(),
|
||||
};
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
login: async ({ request, locals }) => {
|
||||
const { formData, errors } = await validateData(
|
||||
await request.formData(),
|
||||
loginUserSchema
|
||||
);
|
||||
|
||||
if (errors) {
|
||||
return fail(400, {
|
||||
data: formData,
|
||||
errors: errors.fieldErrors,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await locals.pb
|
||||
.collection("users")
|
||||
.authWithPassword(formData.email, formData.password);
|
||||
|
||||
/*
|
||||
if (!locals.pb?.authStore?.model?.verified) {
|
||||
locals.pb.authStore.clear();
|
||||
return {
|
||||
notVerified: true,
|
||||
};
|
||||
}
|
||||
*/
|
||||
} catch (err) {
|
||||
console.log("Error: ", err);
|
||||
error(err.status, err.message);
|
||||
}
|
||||
|
||||
redirect(303, "/");
|
||||
},
|
||||
|
||||
register: async ({ locals, request }) => {
|
||||
const { formData, errors } = await validateData(
|
||||
await request.formData(),
|
||||
registerUserSchema
|
||||
);
|
||||
|
||||
if (errors) {
|
||||
return fail(400, {
|
||||
data: formData,
|
||||
errors: errors.fieldErrors,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
let newUser = await locals.pb.collection("users").create(formData);
|
||||
/*
|
||||
await locals.pb?.collection('users').update(
|
||||
newUser?.id, {
|
||||
'freeTrial' : true,
|
||||
'tier': 'Pro', //Give new users a free trial for the Pro Subscription
|
||||
});
|
||||
*/
|
||||
await locals.pb.collection("users").requestVerification(formData.email);
|
||||
} catch (err) {
|
||||
console.log("Error: ", err);
|
||||
error(err.status, err.message);
|
||||
}
|
||||
|
||||
try {
|
||||
await locals.pb
|
||||
.collection("users")
|
||||
.authWithPassword(formData.email, formData.password);
|
||||
} catch (err) {
|
||||
console.log("Error: ", err);
|
||||
error(err.status, err.message);
|
||||
}
|
||||
|
||||
redirect(303, "/");
|
||||
},
|
||||
|
||||
oauth2: async ({ url, locals, request, cookies }) => {
|
||||
const authMethods = await locals?.pb
|
||||
?.collection("users")
|
||||
?.listAuthMethods();
|
||||
|
||||
const data = await request?.formData();
|
||||
const providerSelected = data?.get("provider");
|
||||
|
||||
if (!authMethods) {
|
||||
return {
|
||||
authProviderRedirect: "",
|
||||
authProviderState: "",
|
||||
};
|
||||
}
|
||||
const redirectURL = `${url.origin}/oauth`;
|
||||
|
||||
const targetItem = authMethods.authProviders?.findIndex(
|
||||
(item) => item?.name === providerSelected
|
||||
);
|
||||
//console.log("==================")
|
||||
//console.log(authMethods.authProviders)
|
||||
//console.log('target item is: ', targetItem)
|
||||
|
||||
const provider = authMethods.authProviders[targetItem];
|
||||
const authProviderRedirect = `${provider.authUrl}${redirectURL}`;
|
||||
const state = provider.state;
|
||||
const verifier = provider.codeVerifier;
|
||||
|
||||
cookies.set("state", state, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
path: "/",
|
||||
maxAge: 60 * 60,
|
||||
});
|
||||
|
||||
cookies.set("verifier", verifier, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
path: "/",
|
||||
maxAge: 60 * 60,
|
||||
});
|
||||
|
||||
cookies.set("provider", providerSelected, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
path: "/",
|
||||
maxAge: 60 * 60,
|
||||
});
|
||||
|
||||
cookies.set("path", "/community", {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
path: "/",
|
||||
maxAge: 60,
|
||||
});
|
||||
|
||||
redirect(302, authProviderRedirect);
|
||||
},
|
||||
};
|
||||
@ -1,878 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import {
|
||||
postVote,
|
||||
discordMembers,
|
||||
cachedPosts,
|
||||
goBackToPostId,
|
||||
numberOfUnreadNotification,
|
||||
postIdDeleted,
|
||||
} from "$lib/store";
|
||||
|
||||
import { afterNavigate } from "$app/navigation";
|
||||
import { base } from "$app/paths";
|
||||
import CreateNewPost from "$lib/components/CreateNewPost.svelte";
|
||||
import PostSection from "$lib/components/PostSection.svelte";
|
||||
import SkeletonLoading from "$lib/components/SkeletonLoading.svelte";
|
||||
import InfiniteLoading from "$lib/components/InfiniteLoading.svelte";
|
||||
import communityBanner from "$lib/images/community_banner.jpg";
|
||||
import * as Avatar from "$lib/components/shadcn/avatar/index.js";
|
||||
|
||||
export let data;
|
||||
export let form;
|
||||
|
||||
let discordURL = import.meta.env.VITE_DISCORD_URL;
|
||||
|
||||
let isLoaded = false;
|
||||
|
||||
let moderators = data?.getModerators;
|
||||
let communityStats = data?.getCommunityStats;
|
||||
let discordData = data?.getDiscordWidget;
|
||||
let posts = null;
|
||||
|
||||
let currentPage = 1;
|
||||
let postLoading = false;
|
||||
let seenPostId = [];
|
||||
|
||||
let noPostMore = false;
|
||||
|
||||
async function infiniteHandler({ detail: { loaded, complete } }) {
|
||||
// console.log("Page position:", window.pageYOffset);
|
||||
seenPostId = posts?.map((obj) => obj?.id);
|
||||
|
||||
if (!postLoading && !noPostMore) {
|
||||
postLoading = true;
|
||||
|
||||
const newPosts = await getPost();
|
||||
if (newPosts.length === 0) {
|
||||
noPostMore = true;
|
||||
complete();
|
||||
} else {
|
||||
// Remove new posts with duplicate IDs
|
||||
const filteredNewPosts = newPosts.filter(
|
||||
(newPost) => !posts.find((post) => post.id === newPost.id),
|
||||
);
|
||||
posts = [...posts, ...filteredNewPosts];
|
||||
currentPage++;
|
||||
loaded();
|
||||
}
|
||||
postLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getPost() {
|
||||
let postData;
|
||||
|
||||
if (Object?.keys($cachedPosts)?.length === 0) {
|
||||
postData = {
|
||||
//userId: data?.user.id,
|
||||
startPage: currentPage,
|
||||
seenPostId: seenPostId.length === 0 ? [] : seenPostId,
|
||||
sortingPosts: sortingPosts,
|
||||
};
|
||||
} else {
|
||||
postData = {
|
||||
//userId: data?.user.id,
|
||||
startPage: $cachedPosts?.currentPage,
|
||||
seenPostId:
|
||||
$cachedPosts?.seenPostId.length === 0 ? [] : $cachedPosts?.seenPostId,
|
||||
sortingPosts: sortingPosts,
|
||||
};
|
||||
}
|
||||
|
||||
// Make the POST request to the endpoint
|
||||
const response = await fetch("/api/get-post", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(postData),
|
||||
});
|
||||
|
||||
const output = await response.json();
|
||||
|
||||
return output?.items;
|
||||
}
|
||||
|
||||
let LoginPopup;
|
||||
let BottomNavigation;
|
||||
|
||||
onMount(async () => {
|
||||
if (Object?.keys($cachedPosts)?.length === 0) {
|
||||
// Only make API requests if cached posts are not available
|
||||
[posts] = await Promise.all([
|
||||
getPost(),
|
||||
//getTickerMentioning(),
|
||||
]);
|
||||
} else {
|
||||
// Use cached data if available
|
||||
posts = $cachedPosts?.posts;
|
||||
}
|
||||
|
||||
if (!data?.user) {
|
||||
LoginPopup = (await import("$lib/components/LoginPopup.svelte")).default;
|
||||
}
|
||||
|
||||
BottomNavigation = (await import("$lib/components/BottomNavigation.svelte"))
|
||||
.default;
|
||||
|
||||
isLoaded = true;
|
||||
});
|
||||
|
||||
onDestroy(async () => {
|
||||
$postIdDeleted = "";
|
||||
$postVote = {};
|
||||
$goBackToPostId = "";
|
||||
});
|
||||
|
||||
let sortingPosts =
|
||||
$cachedPosts?.sortingPosts?.length > 0 ? $cachedPosts?.sortingPosts : "hot";
|
||||
|
||||
async function handleCategoryOfPosts(state) {
|
||||
isLoaded = false;
|
||||
posts = [];
|
||||
currentPage = 1;
|
||||
postLoading = false;
|
||||
seenPostId = [];
|
||||
noPostMore = false;
|
||||
|
||||
sortingPosts = state;
|
||||
$cachedPosts = {};
|
||||
posts = await getPost();
|
||||
isLoaded = true;
|
||||
}
|
||||
|
||||
let goBackToPosition = false;
|
||||
|
||||
let previousPage: string = base;
|
||||
|
||||
afterNavigate(({ from }) => {
|
||||
previousPage = from?.url.pathname || previousPage;
|
||||
|
||||
if (previousPage.startsWith("/community/post")) {
|
||||
goBackToPosition = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Function to update the vote
|
||||
function updateVote(posts, postVote) {
|
||||
const { id, upvote, downvote, upvoteClicked, downvoteClicked } = postVote;
|
||||
|
||||
// Find the post by ID
|
||||
const post = posts?.find((post) => post?.id === id);
|
||||
|
||||
if (post) {
|
||||
post.upvote = upvote;
|
||||
post.downvote = downvote;
|
||||
// Check if expand['alreadyVoted(post)'] exists
|
||||
if (
|
||||
post?.expand["alreadyVoted(post)"] &&
|
||||
post?.expand["alreadyVoted(post)"]?.length > 0
|
||||
) {
|
||||
// Find the vote entry for the current user, if it exists
|
||||
const userVote = post?.expand["alreadyVoted(post)"]?.find(
|
||||
(vote) => vote.user === data?.user?.id,
|
||||
);
|
||||
|
||||
if (userVote) {
|
||||
// Update the existing vote for the user
|
||||
userVote.type = upvoteClicked
|
||||
? "upvote"
|
||||
: downvoteClicked
|
||||
? "downvote"
|
||||
: "neutral";
|
||||
} else {
|
||||
// If no vote entry for the user, add a new one
|
||||
post.expand["alreadyVoted(post)"]?.push({
|
||||
type: upvoteClicked
|
||||
? "upvote"
|
||||
: downvoteClicked
|
||||
? "downvote"
|
||||
: "neutral",
|
||||
user: data?.user?.id,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Create the structure if it does not exist
|
||||
post.expand["alreadyVoted(post)"] = [
|
||||
{
|
||||
type: upvoteClicked
|
||||
? "upvote"
|
||||
: downvoteClicked
|
||||
? "downvote"
|
||||
: "neutral",
|
||||
user: data?.user?.id,
|
||||
},
|
||||
];
|
||||
}
|
||||
} else {
|
||||
console.log("Post not found.");
|
||||
}
|
||||
return posts;
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($postIdDeleted.length !== 0) {
|
||||
posts = posts?.filter((item) => item?.id !== $postIdDeleted);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($postVote && Object?.keys($postVote).length !== 0 && data?.user?.id) {
|
||||
//Update in realtime the already downloaded posts list when user votes
|
||||
posts = updateVote(posts, $postVote);
|
||||
//console.log(posts?.at(0))
|
||||
$postVote = {};
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (posts) {
|
||||
$cachedPosts = {
|
||||
sortingPosts: sortingPosts,
|
||||
currentPage: currentPage,
|
||||
seenPostId: seenPostId,
|
||||
posts: posts,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Reactive statement to check and scroll to the target post
|
||||
$: if (
|
||||
$goBackToPostId?.length !== 0 &&
|
||||
$cachedPosts?.posts?.length !== 0 &&
|
||||
typeof window !== "undefined"
|
||||
) {
|
||||
const postExists = $cachedPosts?.posts?.find(
|
||||
(post) => post?.id === $goBackToPostId,
|
||||
);
|
||||
isLoaded = false;
|
||||
if (postExists) {
|
||||
scrollToPost($goBackToPostId);
|
||||
$goBackToPostId = "";
|
||||
}
|
||||
isLoaded = true;
|
||||
}
|
||||
|
||||
function scrollToPost(postId) {
|
||||
// Add a small delay to ensure DOM is updated
|
||||
setTimeout(() => {
|
||||
const targetDiv = document.getElementById(`post-${postId}`);
|
||||
if (targetDiv) {
|
||||
targetDiv.scrollIntoView(true);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{$numberOfUnreadNotification > 0 ? `(${$numberOfUnreadNotification})` : ""} A
|
||||
community for discussion, insights, news and memes about financial markets. ·
|
||||
stocknear</title
|
||||
>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="A community for discussion, insights, news and memes about financial markets."
|
||||
/>
|
||||
<!-- Other meta tags -->
|
||||
<meta
|
||||
property="og:title"
|
||||
content="A community for discussion, insights, news and memes about financial markets. · stocknear"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="A community for discussion, insights, news and memes about financial markets."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<!-- Add more Open Graph meta tags as needed -->
|
||||
|
||||
<!-- Twitter specific meta tags -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="A community for discussion, insights, news and memes about financial markets. · stocknear"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="A community for discussion, insights, news and memes about financial markets."
|
||||
/>
|
||||
<!-- Add more Twitter meta tags as needed -->
|
||||
</svelte:head>
|
||||
|
||||
<body class="bg-[#09090B] sm:mt-5 max-w-3xl sm:max-w-screen-2xl">
|
||||
<!-- Page wrapper -->
|
||||
<div
|
||||
class="max-w-screen-xl flex flex-col min-h-screen overflow-hidden pl-0 lg:pl-20 m-auto w-full supports-[overflow:clip]:overflow-clip pb-40"
|
||||
>
|
||||
<main class="m-auto w-full">
|
||||
<!--Start Header-->
|
||||
<div
|
||||
class="w-full sm:rounded-md m-auto h-44 sm:h-60 shadow-sm shadow-black bg-center bg-cover bg-no-repeat"
|
||||
style="background-image: url('{communityBanner}');"
|
||||
/>
|
||||
<!--End Header-->
|
||||
|
||||
<!-- Page content -->
|
||||
<section>
|
||||
<div class="w-full">
|
||||
<div class="sm:flex sm:justify-start w-full">
|
||||
<!-- Main content -->
|
||||
<div class="sm:pt-6 pb-12 w-full">
|
||||
<div class="lg:pr-5">
|
||||
<!--<CreateNewPost />-->
|
||||
<div class={isLoaded ? "hidden" : ""}>
|
||||
{#each Array(5) as _}
|
||||
<SkeletonLoading />
|
||||
{/each}
|
||||
</div>
|
||||
{#if posts !== null}
|
||||
<div class="">
|
||||
<CreateNewPost data={data?.user} />
|
||||
</div>
|
||||
|
||||
<!--Start choose Hot or New Posts-->
|
||||
<!-- List container -->
|
||||
<div class="flex flex-col mt-4">
|
||||
<!-- Item -->
|
||||
<div
|
||||
class="border-t border-b sm:border border-gray-700 sm:hover:border-gray-600 rounded-none sm:rounded-md bg-[#141417] rounded-[4px] sm:rounded-[8px]"
|
||||
>
|
||||
<div class="flex h-14 justify-start items-center">
|
||||
<div class="flex flex-row ml-3">
|
||||
<label
|
||||
on:click={() => handleCategoryOfPosts("hot")}
|
||||
class="flex flex-row w-fit pl-3 pr-4 pt-1 pb-1 tab mr-2 font-medium transition duration-150 ease-out hover:ease-in rounded-full hover:bg-[#323232] {sortingPosts ===
|
||||
'hot'
|
||||
? 'bg-[#323232] text-white'
|
||||
: 'text-gray-300'} rounded-full cursor-pointer"
|
||||
>
|
||||
<svg
|
||||
class="w-5 h-5 sm:w-6 sm:h-6 inline-block"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill={sortingPosts === "hot"
|
||||
? "red"
|
||||
: "#737487"}
|
||||
d="M17.66 11.2c-.23-.3-.51-.56-.77-.82c-.67-.6-1.43-1.03-2.07-1.66C13.33 7.26 13 4.85 13.95 3c-.95.23-1.78.75-2.49 1.32c-2.59 2.08-3.61 5.75-2.39 8.9c.04.1.08.2.08.33c0 .22-.15.42-.35.5c-.23.1-.47.04-.66-.12a.58.58 0 0 1-.14-.17c-1.13-1.43-1.31-3.48-.55-5.12C5.78 10 4.87 12.3 5 14.47c.06.5.12 1 .29 1.5c.14.6.41 1.2.71 1.73c1.08 1.73 2.95 2.97 4.96 3.22c2.14.27 4.43-.12 6.07-1.6c1.83-1.66 2.47-4.32 1.53-6.6l-.13-.26c-.21-.46-.77-1.26-.77-1.26m-3.16 6.3c-.28.24-.74.5-1.1.6c-1.12.4-2.24-.16-2.9-.82c1.19-.28 1.9-1.16 2.11-2.05c.17-.8-.15-1.46-.28-2.23c-.12-.74-.1-1.37.17-2.06c.19.38.39.76.63 1.06c.77 1 1.98 1.44 2.24 2.8c.04.14.06.28.06.43c.03.82-.33 1.72-.93 2.27Z"
|
||||
/></svg
|
||||
>
|
||||
<p class="ml-1 font-bold text-sm sm:text-[1rem]">
|
||||
Hot
|
||||
</p>
|
||||
</label>
|
||||
|
||||
<label
|
||||
on:click={() => handleCategoryOfPosts("new")}
|
||||
class="flex flex-row w-fit ml-3 pl-3 pr-4 pt-1 pb-1 tab mr-2 font-medium transition duration-150 ease-out hover:ease-in rounded-full hover:bg-[#323232] {sortingPosts ===
|
||||
'new'
|
||||
? 'bg-[#323232] text-white'
|
||||
: 'text-gray-300'} rounded-full cursor-pointer"
|
||||
>
|
||||
<svg
|
||||
class="w-5 h-5 sm:w-6 sm:h-6 inline-block"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g
|
||||
id="SVGRepo_tracerCarrier"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></g><g id="SVGRepo_iconCarrier">
|
||||
<path
|
||||
d="M15.3895 5.21125L16.7995 8.03125C16.9895 8.42125 17.4995 8.79125 17.9295 8.87125L20.4795 9.29125C22.1095 9.56125 22.4895 10.7413 21.3195 11.9213L19.3295 13.9113C18.9995 14.2413 18.8095 14.8913 18.9195 15.3613L19.4895 17.8213C19.9395 19.7613 18.8995 20.5213 17.1895 19.5013L14.7995 18.0813C14.3695 17.8213 13.6495 17.8213 13.2195 18.0813L10.8295 19.5013C9.11945 20.5113 8.07945 19.7613 8.52945 17.8213L9.09945 15.3613C9.18945 14.8813 8.99945 14.2313 8.66945 13.9013L6.67945 11.9113C5.50945 10.7413 5.88945 9.56125 7.51945 9.28125L10.0695 8.86125C10.4995 8.79125 11.0095 8.41125 11.1995 8.02125L12.6095 5.20125C13.3795 3.68125 14.6195 3.68125 15.3895 5.21125Z"
|
||||
fill={sortingPosts === "new"
|
||||
? "#D6B329"
|
||||
: "#737487"}
|
||||
></path>
|
||||
<path
|
||||
d="M8 5.75H2C1.59 5.75 1.25 5.41 1.25 5C1.25 4.59 1.59 4.25 2 4.25H8C8.41 4.25 8.75 4.59 8.75 5C8.75 5.41 8.41 5.75 8 5.75Z"
|
||||
fill={sortingPosts === "new"
|
||||
? "#D6B329"
|
||||
: "#737487"}
|
||||
></path>
|
||||
<path
|
||||
d="M5 19.75H2C1.59 19.75 1.25 19.41 1.25 19C1.25 18.59 1.59 18.25 2 18.25H5C5.41 18.25 5.75 18.59 5.75 19C5.75 19.41 5.41 19.75 5 19.75Z"
|
||||
fill={sortingPosts === "new"
|
||||
? "#D6B329"
|
||||
: "#737487"}
|
||||
></path>
|
||||
<path
|
||||
d="M3 12.75H2C1.59 12.75 1.25 12.41 1.25 12C1.25 11.59 1.59 11.25 2 11.25H3C3.41 11.25 3.75 11.59 3.75 12C3.75 12.41 3.41 12.75 3 12.75Z"
|
||||
fill={sortingPosts === "new"
|
||||
? "#D6B329"
|
||||
: "#737487"}
|
||||
></path>
|
||||
</g></svg
|
||||
>
|
||||
<p class="ml-1 font-bold text-sm sm:text-[1rem]">
|
||||
New
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End choose Hot or New Posts-->
|
||||
|
||||
{#if isLoaded}
|
||||
{#each posts as post}
|
||||
<div class="flex items-start w-full">
|
||||
<div id="post-{post.id}" class="w-full m-auto">
|
||||
<PostSection {data} posts={post} {moderators} />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<InfiniteLoading on:infinite={infiniteHandler} />
|
||||
{/if}
|
||||
|
||||
{#if postLoading && !noPostMore && data?.user}
|
||||
<SkeletonLoading />
|
||||
{:else if noPostMore}
|
||||
<div
|
||||
class=" mt-10 flex flex-col justify-center items-center text-lg sm:text-xl font-bold text-gray-400 mb-20 m-auto"
|
||||
>
|
||||
We reached the bottom brother
|
||||
<p class="mt-2 text-md text-gray-400 font-medium">
|
||||
Share your insight with others
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="tooltip tooltip-bottom mt-2 break-all"
|
||||
data-tip="Create a Post"
|
||||
>
|
||||
{#if !data?.user}
|
||||
<label
|
||||
for="userLogin"
|
||||
class="flex rounded-full w-fit h-fit cursor-pointer"
|
||||
>
|
||||
<svg
|
||||
class="m-auto mt-1 w-10 h-10"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="#78818E"
|
||||
d="M11 13v3q0 .425.288.713T12 17q.425 0 .713-.288T13 16v-3h3q.425 0 .713-.288T17 12q0-.425-.288-.713T16 11h-3V8q0-.425-.288-.713T12 7q-.425 0-.713.288T11 8v3H8q-.425 0-.713.288T7 12q0 .425.288.713T8 13h3Zm1 9q-2.075 0-3.9-.788t-3.175-2.137q-1.35-1.35-2.137-3.175T2 12q0-2.075.788-3.9t2.137-3.175q1.35-1.35 3.175-2.137T12 2q2.075 0 3.9.788t3.175 2.137q1.35 1.35 2.138 3.175T22 12q0 2.075-.788 3.9t-2.137 3.175q-1.35 1.35-3.175 2.138T12 22Z"
|
||||
/></svg
|
||||
>
|
||||
</label>
|
||||
{:else}
|
||||
<a
|
||||
href="/community/create-post/"
|
||||
class="flex rounded-full w-fit h-fit"
|
||||
>
|
||||
<svg
|
||||
class="m-auto mt-1 w-10 h-10"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="#78818E"
|
||||
d="M11 13v3q0 .425.288.713T12 17q.425 0 .713-.288T13 16v-3h3q.425 0 .713-.288T17 12q0-.425-.288-.713T16 11h-3V8q0-.425-.288-.713T12 7q-.425 0-.713.288T11 8v3H8q-.425 0-.713.288T7 12q0 .425.288.713T8 13h3Zm1 9q-2.075 0-3.9-.788t-3.175-2.137q-1.35-1.35-2.137-3.175T2 12q0-2.075.788-3.9t2.137-3.175q1.35-1.35 3.175-2.137T12 2q2.075 0 3.9.788t3.175 2.137q1.35 1.35 2.138 3.175T22 12q0 2.075-.788 3.9t-2.137 3.175q-1.35 1.35-3.175 2.138T12 22Z"
|
||||
/></svg
|
||||
>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
class="hidden lg:inline-block h-sh lg:w-1/2 pt-[1.5rem] pr-0 lg:pr-2 xl:pr-0"
|
||||
>
|
||||
{#if isLoaded}
|
||||
<!-- Sidebar content -->
|
||||
<div class="lg:pl-2 z-20 h-full">
|
||||
<!-- Sidebar content -->
|
||||
|
||||
<!--Start About Community -->
|
||||
<div class="space-y-3 mb-5">
|
||||
<div
|
||||
class="rounded-t-lg bg-[#141417] border border-gray-800 h-auto sm:w-96"
|
||||
>
|
||||
<!--Start Header-->
|
||||
<div class="bg-[#141417] w-full p-3 rounded-t-lg">
|
||||
<span class="text-white text-xl ml-1 font-semibold">
|
||||
About Community
|
||||
</span>
|
||||
</div>
|
||||
<!--End Header-->
|
||||
<!--Start Content-->
|
||||
<div class="w-full p-4 -mt-2 text-white">
|
||||
A community for discussion, insights, news and memes
|
||||
about financial markets.
|
||||
</div>
|
||||
<!--End Content-->
|
||||
|
||||
<div
|
||||
class="mt-4 mb-4 border-t border-slate-700 w-11/12 m-auto"
|
||||
/>
|
||||
<!--Start Content-->
|
||||
<div
|
||||
class="w-full pl-5 pt-2 pb-2 flex flex-row items-center"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-white font-bold text-lg">
|
||||
{communityStats?.totalUsers}
|
||||
</span>
|
||||
<span
|
||||
class="text-white text-opacity-[0.7] text-[1rem"
|
||||
>
|
||||
Members
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col ml-12">
|
||||
<span class="text-white font-bold text-lg">
|
||||
{communityStats?.totalPosts}
|
||||
</span>
|
||||
<span
|
||||
class="text-white text-opacity-[0.7] text-[1rem"
|
||||
>
|
||||
Posts
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col ml-12">
|
||||
<span class="text-white font-bold text-lg">
|
||||
{communityStats?.totalComments}
|
||||
</span>
|
||||
<span
|
||||
class="text-white text-opacity-[0.7] text-[1rem"
|
||||
>
|
||||
Comments
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Content-->
|
||||
|
||||
<div
|
||||
class="mt-4 border-t border-slate-700 w-11/12 m-auto"
|
||||
/>
|
||||
|
||||
<div class="flex justify-center items-center mb-8 pt-5">
|
||||
<a
|
||||
href="/community/create-post"
|
||||
class="rounded-md cursor-pointer w-11/12 py-2 h-full mt-2 text-md text-center font-semibold text-black m-auto sm:hover:bg-gray-300 bg-[#fff] mb-6 duration-100"
|
||||
>
|
||||
Create Post
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--End About Community -->
|
||||
|
||||
<!--Start Discord -->
|
||||
{#if discordData?.length !== 0}
|
||||
<div class="space-y-3 mb-5">
|
||||
<div
|
||||
class="bg-[#141417] border border-gray-800 h-auto sm:w-96 rounded-md"
|
||||
>
|
||||
<!--Start Image-->
|
||||
<div class="flex flex-row items-center w-full p-3">
|
||||
<svg
|
||||
class="w-12 mt-3 ml-2 inline-block"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="white"
|
||||
d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.09.09 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.09 16.09 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09c-.01-.02-.04-.03-.07-.03c-1.5.26-2.93.71-4.27 1.33c-.01 0-.02.01-.03.02c-2.72 4.07-3.47 8.03-3.1 11.95c0 .02.01.04.03.05c1.8 1.32 3.53 2.12 5.24 2.65c.03.01.06 0 .07-.02c.4-.55.76-1.13 1.07-1.74c.02-.04 0-.08-.04-.09c-.57-.22-1.11-.48-1.64-.78c-.04-.02-.04-.08-.01-.11c.11-.08.22-.17.33-.25c.02-.02.05-.02.07-.01c3.44 1.57 7.15 1.57 10.55 0c.02-.01.05-.01.07.01c.11.09.22.17.33.26c.04.03.04.09-.01.11c-.52.31-1.07.56-1.64.78c-.04.01-.05.06-.04.09c.32.61.68 1.19 1.07 1.74c.03.01.06.02.09.01c1.72-.53 3.45-1.33 5.25-2.65c.02-.01.03-.03.03-.05c.44-4.53-.73-8.46-3.1-11.95c-.01-.01-.02-.02-.04-.02M8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.84 2.12-1.89 2.12m6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.83 2.12-1.89 2.12"
|
||||
/></svg
|
||||
>
|
||||
<div
|
||||
class="flex justify-center items-center text-2xl font-medium ml-3 mt-3"
|
||||
>
|
||||
<span
|
||||
class="self-center text-2xl text-white font-bold whitespace-nowrap"
|
||||
>
|
||||
Official Discord
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Image-->
|
||||
<!--Start Content-->
|
||||
<div class="w-full p-4 text-white">
|
||||
Join our official Discord community to connect with me
|
||||
and fellow members. Let's chat, share ideas, and grow
|
||||
together!
|
||||
</div>
|
||||
<!--End Content-->
|
||||
|
||||
<div
|
||||
class="mt-4 mb-4 border-t border-slate-700 w-11/12 m-auto"
|
||||
/>
|
||||
<!--Start Content-->
|
||||
<div
|
||||
class="w-full pl-5 pt-2 pb-2 flex flex-row items-center"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-white font-bold text-lg">
|
||||
{$discordMembers}
|
||||
</span>
|
||||
<span
|
||||
class="text-white text-opacity-[0.7] text-[1rem"
|
||||
>
|
||||
Members
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col ml-12">
|
||||
<div
|
||||
class="flex flex-row items-center text-white font-bold text-lg"
|
||||
>
|
||||
<div
|
||||
class="mr-1 inline-block w-3.5 h-3.5 sm:w-4 sm:h-4 bg-[#75D377] border border-2 border-slate-800 rounded-full"
|
||||
/>
|
||||
{discordData?.members?.length}
|
||||
</div>
|
||||
<span
|
||||
class="text-white text-opacity-[0.7] text-[1rem"
|
||||
>
|
||||
Online
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="ml-12 avatar-group -space-x-6 rtl:space-x-reverse"
|
||||
>
|
||||
{#each discordData?.members?.slice(0, 5) as item, index}
|
||||
{#if index < 4}
|
||||
<Avatar.Root>
|
||||
<Avatar.Image src={item["avatar_url"]} />
|
||||
<Avatar.Fallback>CN</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
{/if}
|
||||
{#if index >= 4}
|
||||
<Avatar.Root class="bg-neutral relative">
|
||||
<div
|
||||
class="w-8 h-8 flex-shrink-0 text-sm text-center text-white flex items-center justify-center"
|
||||
>
|
||||
<span
|
||||
class="absolute transform -translate-x-1/2 -translate-y-1/2 left-1/2 top-1/2"
|
||||
>+{discordData?.members?.length - 4}</span
|
||||
>
|
||||
</div>
|
||||
</Avatar.Root>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--End Content-->
|
||||
|
||||
<div
|
||||
class="mt-4 border-t border-slate-700 w-11/12 m-auto"
|
||||
/>
|
||||
|
||||
<div class="flex justify-center items-center mb-8 pt-5">
|
||||
<a
|
||||
href={discordURL}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
class="rounded-md cursor-pointer w-11/12 py-2 h-full mt-2 text-md text-center font-semibold text-black m-auto sm:hover:bg-gray-300 bg-[#fff] mb-6 duration-100"
|
||||
>
|
||||
Join Us
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!--End Discord -->
|
||||
|
||||
<!--Community Rules-->
|
||||
<div class="space-y-3 mt-5 fixed sticky" style="top: 5rem;">
|
||||
<div
|
||||
class="bg-[#141417] border border-gray-800 sm:w-96 rounded-md"
|
||||
>
|
||||
<!--Start Header-->
|
||||
<div
|
||||
class="bg-[#141417] border-b border-slate-700 w-full pl-6 pr-6 pt-6 pb-4 rounded-t-lg"
|
||||
>
|
||||
<span class="text-white text-xl ml-1 font-semibold">
|
||||
Community Rules
|
||||
</span>
|
||||
</div>
|
||||
<!--End Header-->
|
||||
|
||||
<!--Start Content-->
|
||||
<div class="w-full pl-7 text-white">
|
||||
<ol class="list-decimal pl-4 pr-4 pb-5 pt-4">
|
||||
<li class="text-[1rem] mb-3">
|
||||
Avoid Purely Policital Discussion
|
||||
</li>
|
||||
<li class="text-[1rem] mb-3">
|
||||
No violence or gory contents
|
||||
</li>
|
||||
<li class="text-[1rem] mb-3">
|
||||
No Advertisement, Self-Promotion, Fundraising, or
|
||||
Begging
|
||||
</li>
|
||||
<li class="text-[1rem]">No illegal activities</li>
|
||||
</ol>
|
||||
</div>
|
||||
<!--End Content-->
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-start ml-4 mt-4">
|
||||
<div class="flex flex-row gap-x-3">
|
||||
<a
|
||||
href="/about"
|
||||
class="text-sm text-gray-400 hover:text-gray-300 hover:underline"
|
||||
>
|
||||
About
|
||||
</a>
|
||||
<a
|
||||
href="/terms-of-use"
|
||||
class="text-sm text-gray-400 hover:text-gray-300 hover:underline"
|
||||
>
|
||||
Terms
|
||||
</a>
|
||||
<a
|
||||
href="/privacy-policy"
|
||||
class="text-sm text-gray-400 hover:text-gray-300 hover:underline"
|
||||
>
|
||||
Privacy
|
||||
</a>
|
||||
<a
|
||||
href="/contact"
|
||||
class="text-sm text-gray-400 hover:text-gray-300 hover:underline"
|
||||
>
|
||||
Contact
|
||||
</a>
|
||||
<a
|
||||
href="/imprint"
|
||||
class="text-sm text-gray-400 hover:text-gray-300 hover:underline"
|
||||
>
|
||||
Imprint
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<span class="text-sm text-gray-400 mt-1.5">
|
||||
© 2024 stocknear
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!--End Community Rules-->
|
||||
</div>
|
||||
{/if}
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
{#if BottomNavigation}
|
||||
<BottomNavigation data={data} />
|
||||
{/if}
|
||||
-->
|
||||
|
||||
<!--Start Login Modal-->
|
||||
{#if LoginPopup}
|
||||
<LoginPopup {form} />
|
||||
{/if}
|
||||
<!--End Login Modal-->
|
||||
</body>
|
||||
|
||||
<style>
|
||||
.pageTransitionOut {
|
||||
view-transition-name: pageTransitionOut;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-from-right {
|
||||
from {
|
||||
transform: translateX(500px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-to-left {
|
||||
to {
|
||||
transform: translateX(-200px);
|
||||
}
|
||||
}
|
||||
|
||||
:root::view-transition-old(root) {
|
||||
animation:
|
||||
180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
|
||||
230ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
|
||||
}
|
||||
|
||||
:root::view-transition-new(root) {
|
||||
animation:
|
||||
210ms cubic-bezier(0, 0, 0.2, 1) 180ms both fade-in,
|
||||
230ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root::view-transition-old(root) {
|
||||
animation:
|
||||
180ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
|
||||
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
|
||||
}
|
||||
|
||||
:root::view-transition-new(root) {
|
||||
animation:
|
||||
210ms cubic-bezier(0, 0, 0.2, 1) 180ms both fade-in,
|
||||
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
|
||||
}
|
||||
}
|
||||
|
||||
.vote-counter {
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
|
||||
.vote-counter.changed {
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
|
||||
.grid-background {
|
||||
background: linear-gradient(to bottom, #404040, #14284b);
|
||||
width: 24rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-background::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-radius: 10%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 0.15) 0,
|
||||
rgba(255, 255, 255, 0.15) 1px,
|
||||
transparent 1px,
|
||||
transparent 40px
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
to right,
|
||||
rgba(255, 255, 255, 0.15) 0,
|
||||
rgba(255, 255, 255, 0.15) 1px,
|
||||
transparent 1px,
|
||||
transparent 40px
|
||||
);
|
||||
background-size: 20px 100% 100% 20px; /* Adjust the grid spacing as needed */
|
||||
}
|
||||
</style>
|
||||
@ -1,364 +0,0 @@
|
||||
import {
|
||||
createPostTextSchema,
|
||||
createPostImageSchema,
|
||||
createPostLinkSchema,
|
||||
} from "$lib/schemas";
|
||||
import { validateData } from "$lib/utils";
|
||||
import { error, fail, redirect } from "@sveltejs/kit";
|
||||
import { serialize } from "object-to-formdata";
|
||||
import { marked } from "marked";
|
||||
|
||||
import got from "got";
|
||||
import cheerio from "cheerio";
|
||||
import * as blobUtil from "blob-util"; // Changed import
|
||||
import sharp from "sharp";
|
||||
|
||||
function removeDuplicateClasses(str) {
|
||||
return str.replace(
|
||||
/class="([^"]*)"/,
|
||||
(match, classAttr) =>
|
||||
`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;
|
||||
}
|
||||
|
||||
export const load = async ({ locals }) => {
|
||||
if (!locals.pb.authStore.isValid) {
|
||||
redirect(303, "/login");
|
||||
}
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
createPostText: async ({ request, locals }) => {
|
||||
let newPost = "";
|
||||
const body = await request.formData();
|
||||
body.delete("thumbnail");
|
||||
body.append("user", locals?.user?.id);
|
||||
|
||||
let { formData, errors } = await validateData(body, createPostTextSchema);
|
||||
|
||||
formData.description = addClassesToHtml(marked(formData?.description));
|
||||
|
||||
formData.tagTopic = JSON.parse(formData.tagTopic)[0];
|
||||
formData.upvote = 1;
|
||||
|
||||
if (errors) {
|
||||
return fail(400, {
|
||||
data: formData,
|
||||
errors: errors.fieldErrors,
|
||||
});
|
||||
}
|
||||
|
||||
//Each post gives the user +1 Karma points
|
||||
await locals.pb.collection("users").update(locals.user.id, {
|
||||
"karma+": 1,
|
||||
});
|
||||
|
||||
try {
|
||||
newPost = await locals.pb.collection("posts").create(serialize(formData));
|
||||
|
||||
// add the tagTopic manually because serialize does not work on arrays
|
||||
await locals?.pb.collection("posts").update(newPost.id, {
|
||||
tagTopic: formData.tagTopic,
|
||||
});
|
||||
|
||||
//Save it collection alreadyVoted to keep track if user voted or not
|
||||
//FormData for alreadyVoted
|
||||
|
||||
let formDataAlreadyVoted = new FormData();
|
||||
formDataAlreadyVoted.append("post", newPost?.id);
|
||||
formDataAlreadyVoted.append("user", newPost?.user);
|
||||
formDataAlreadyVoted.append("type", "upvote");
|
||||
//console.log(formDataAlreadyVoted)
|
||||
await locals.pb.collection("alreadyVoted").create(formDataAlreadyVoted);
|
||||
} catch (err) {
|
||||
console.log("Error: ", err);
|
||||
error(err.status, err.message);
|
||||
}
|
||||
|
||||
if (newPost?.id?.length !== 0) {
|
||||
redirect(303, "/community/post/" + newPost?.id);
|
||||
} else {
|
||||
redirect(303, "/community");
|
||||
}
|
||||
},
|
||||
|
||||
createPostImage: async ({ request, locals }) => {
|
||||
let newPost = "";
|
||||
const body = await request.formData();
|
||||
const thumb = body.get("thumbnail");
|
||||
|
||||
if (thumb?.size === 0) {
|
||||
body.delete("thumbnail");
|
||||
}
|
||||
|
||||
body.append("user", locals?.user?.id);
|
||||
|
||||
let { formData, errors } = await validateData(body, createPostImageSchema);
|
||||
|
||||
formData.tagTopic = JSON.parse(formData.tagTopic)[0];
|
||||
formData.upvote = 1;
|
||||
|
||||
if (errors) {
|
||||
return fail(400, {
|
||||
data: formData,
|
||||
errors: errors.fieldErrors,
|
||||
});
|
||||
}
|
||||
|
||||
//Each post gives the user +1 Karma points
|
||||
await locals.pb.collection("users").update(locals?.user?.id, {
|
||||
"karma+": 1,
|
||||
});
|
||||
|
||||
if (formData.thumbnail.type.includes("image")) {
|
||||
try {
|
||||
const image = formData.thumbnail;
|
||||
const imageBuffer = await image.arrayBuffer();
|
||||
const imageBufferArray = new Uint8Array(imageBuffer);
|
||||
let optimizedImageBuffer;
|
||||
|
||||
if (image.type === "image/gif") {
|
||||
// Process GIF files differently
|
||||
optimizedImageBuffer = imageBufferArray;
|
||||
} else {
|
||||
// Process other image formats (e.g., JPEG, PNG) using sharp library
|
||||
optimizedImageBuffer = await sharp(imageBufferArray)
|
||||
.resize({
|
||||
width: 800,
|
||||
height: 1000,
|
||||
fit: sharp.fit.inside,
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.jpeg({ quality: 80 })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
formData.thumbnail = new File([optimizedImageBuffer], image.name, {
|
||||
type: image.type,
|
||||
lastModified: image.lastModified,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log("Error:", err);
|
||||
error(err.status, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
newPost = await locals.pb.collection("posts").create(serialize(formData));
|
||||
|
||||
// add the tagTopic manually because serialize does not work on arrays
|
||||
await locals?.pb.collection("posts").update(newPost.id, {
|
||||
tagTopic: formData?.tagTopic,
|
||||
});
|
||||
|
||||
//Save it collection alreadyVoted to keep track if user voted or not
|
||||
//FormData for alreadyVoted
|
||||
|
||||
let formDataAlreadyVoted = new FormData();
|
||||
formDataAlreadyVoted.append("post", newPost.id);
|
||||
formDataAlreadyVoted.append("user", newPost.user);
|
||||
formDataAlreadyVoted.append("type", "upvote");
|
||||
//console.log(formDataAlreadyVoted)
|
||||
await locals.pb.collection("alreadyVoted").create(formDataAlreadyVoted);
|
||||
} catch (err) {
|
||||
console.log("Error: ", err);
|
||||
error(err.status, err.message);
|
||||
}
|
||||
|
||||
if (newPost?.id?.length !== 0) {
|
||||
redirect(303, "/community/post/" + newPost?.id);
|
||||
} else {
|
||||
redirect(303, "/community");
|
||||
}
|
||||
},
|
||||
|
||||
createPostLink: async ({ request, locals }) => {
|
||||
let newPost = "";
|
||||
const body = await request.formData();
|
||||
const url = body.get("link");
|
||||
let image;
|
||||
let description;
|
||||
let imageBlob;
|
||||
|
||||
try {
|
||||
const response = await got(url, {
|
||||
headers: {
|
||||
"user-agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
|
||||
},
|
||||
responseType: "buffer",
|
||||
});
|
||||
|
||||
const $ = cheerio.load(response.body);
|
||||
//const title = $('head title').text();
|
||||
description = $('head meta[property="og:description"]').attr("content");
|
||||
image = $('head meta[property="og:image"]').attr("content");
|
||||
|
||||
if (!image) {
|
||||
let largestSize = 0;
|
||||
let largestImage = "";
|
||||
$("img").each(async function () {
|
||||
if (
|
||||
$(this).attr("src") &&
|
||||
$(this)
|
||||
.attr("src")
|
||||
.match(/\.(webp|jpg|jpeg|png|gif)$/)
|
||||
) {
|
||||
try {
|
||||
const imageBuffer = await got(image, {
|
||||
headers: {
|
||||
"user-agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
|
||||
},
|
||||
responseType: "buffer",
|
||||
}).then((response) => response.body);
|
||||
imageBlob = await blobUtil.createBlob([imageBuffer], {
|
||||
// Changed usage
|
||||
type: "image/jpeg",
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle the error when getting the image
|
||||
console.error("Error getting image:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
image = largestImage;
|
||||
}
|
||||
|
||||
// Download the image and append it to the form data
|
||||
const imageBuffer = await got(image, {
|
||||
headers: {
|
||||
"user-agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
|
||||
},
|
||||
responseType: "buffer",
|
||||
}).then((response) => response.body);
|
||||
imageBlob = await blobUtil.createBlob([imageBuffer], {
|
||||
// Changed usage
|
||||
type: "image/jpeg",
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
//body.delete('thumbnail')
|
||||
//body.append('thumbnail', imageBlob);
|
||||
|
||||
// Append the title to the form data
|
||||
//body.append('name', title);
|
||||
body.append("user", locals.user.id);
|
||||
body.append("description", description);
|
||||
|
||||
/*
|
||||
const thumb = body.get('thumbnail');
|
||||
|
||||
if (thumb.size === 0) {
|
||||
body.delete('thumbnail');
|
||||
}
|
||||
*/
|
||||
|
||||
let { formData, errors } = await validateData(body, createPostLinkSchema);
|
||||
|
||||
formData.tagTopic = JSON.parse(formData.tagTopic)[0];
|
||||
formData.upvote = 1;
|
||||
|
||||
if (imageBlob?.length !== 0) {
|
||||
formData.thumbnail = imageBlob;
|
||||
}
|
||||
|
||||
if (errors) {
|
||||
return fail(400, {
|
||||
data: formData,
|
||||
errors: errors.fieldErrors,
|
||||
});
|
||||
}
|
||||
|
||||
//Each post gives the user +1 Karma points
|
||||
await locals.pb.collection("users").update(locals?.user?.id, {
|
||||
"karma+": 1,
|
||||
});
|
||||
|
||||
try {
|
||||
newPost = await locals.pb.collection("posts").create(serialize(formData));
|
||||
|
||||
// add the tagTopic manually because serialize does not work on arrays
|
||||
await locals.pb.collection("posts").update(newPost.id, {
|
||||
tagTopic: formData.tagTopic,
|
||||
});
|
||||
|
||||
//Save it collection alreadyVoted to keep track if user voted or not
|
||||
//FormData for alreadyVoted
|
||||
|
||||
let formDataAlreadyVoted = new FormData();
|
||||
formDataAlreadyVoted.append("post", newPost.id);
|
||||
formDataAlreadyVoted.append("user", newPost.user);
|
||||
formDataAlreadyVoted.append("type", "upvote");
|
||||
//console.log(formDataAlreadyVoted)
|
||||
await locals.pb.collection("alreadyVoted").create(formDataAlreadyVoted);
|
||||
} catch (err) {
|
||||
console.log("Error: ", err);
|
||||
error(err.status, err.message);
|
||||
}
|
||||
|
||||
if (newPost?.id?.length !== 0) {
|
||||
redirect(303, "/community/post/" + newPost?.id);
|
||||
} else {
|
||||
redirect(303, "/community");
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -1,284 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from "$app/forms";
|
||||
import Input from "$lib/components/Input.svelte";
|
||||
import TextArea from "$lib/components/TextArea.svelte";
|
||||
import Tags from "$lib/components/Tags.svelte";
|
||||
import toast from "svelte-french-toast";
|
||||
import { numberOfUnreadNotification } from "$lib/store";
|
||||
|
||||
export let form;
|
||||
let isClicked = false;
|
||||
let loading = false;
|
||||
|
||||
const submitPost = () => {
|
||||
loading = true;
|
||||
return async ({ result, update }) => {
|
||||
switch (result.type) {
|
||||
case "redirect":
|
||||
isClicked = true;
|
||||
toast.success("Posted successfully!", {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
await update();
|
||||
break;
|
||||
case "failure":
|
||||
toast.error("Invalid inputs", {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
await update();
|
||||
break;
|
||||
|
||||
case "error":
|
||||
toast.error(result.error.message, {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
break;
|
||||
default:
|
||||
await update();
|
||||
}
|
||||
loading = false;
|
||||
};
|
||||
};
|
||||
|
||||
let postType = "text";
|
||||
|
||||
async function changePostType(state) {
|
||||
switch (state) {
|
||||
case "text":
|
||||
postType = "text";
|
||||
break;
|
||||
case "image":
|
||||
postType = "image";
|
||||
break;
|
||||
case "link":
|
||||
postType = "link";
|
||||
//title = await getTitle()
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{$numberOfUnreadNotification > 0 ? `(${$numberOfUnreadNotification})` : ""} Create
|
||||
a Post · stocknear</title
|
||||
>
|
||||
<meta
|
||||
name="description"
|
||||
content="
|
||||
Create a post to share your insights, latest news and memes to the stocknear community."
|
||||
/>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
class="flex justify-start items-start m-auto max-w-5xl w-full min-h-screen p-2 pt-10 pb-40"
|
||||
>
|
||||
<div class="w-full">
|
||||
<form
|
||||
action={postType === "text"
|
||||
? "?/createPostText"
|
||||
: postType === "image"
|
||||
? "?/createPostImage"
|
||||
: "?/createPostLink"}
|
||||
method="POST"
|
||||
class="flex flex-col space-y-2 w-full items-center"
|
||||
enctype="multipart/form-data"
|
||||
use:enhance={submitPost}
|
||||
>
|
||||
<h3 class="text-3xl font-bold mb-3 text-white">Create a Post</h3>
|
||||
<!--<p class="mt-2 text-lg">We'll need the title, tagline, url, and description</p>-->
|
||||
|
||||
<div
|
||||
class="w-full max-w-3xl pt-10 flex justify-center items-center m-auto pb-5 bg-[#09090B]"
|
||||
style="top: 4rem;"
|
||||
>
|
||||
<!--<svg class="w-4 h-4 sm:w-6 sm:h-6 inline-block mr-0 sm:mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M2.66992 7.16979V5.34979C2.66992 4.19979 3.59992 3.27979 4.73992 3.27979H19.2599C20.4099 3.27979 21.3299 4.20979 21.3299 5.34979V7.16979" stroke="#A6ADBB" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> <path opacity="0.4" d="M12 20.7199V4.10986" stroke="#A6ADBB" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M8.06055 20.7202H15.9405" stroke="#A6ADBB" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>-->
|
||||
|
||||
<div class="flex flex-col items-center mr-5 sm:mr-10">
|
||||
<label
|
||||
class="tab font-semibold hover:text-gray-300 {postType === 'text'
|
||||
? 'text-gray-200'
|
||||
: 'text-[#9A9996]'}"
|
||||
on:click={() => changePostType("text")}
|
||||
>
|
||||
<span class="text-lg sm:text-2xl">Text</span>
|
||||
</label>
|
||||
<div
|
||||
class="{postType === 'text'
|
||||
? 'bg-[#75D377]'
|
||||
: 'bg-[#09090B]'} h-1 w-[3rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!--<svg class="w-5 h-5 sm:w-6 sm:h-6 inline-block mr-0 sm:mr-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path opacity="0.4" d="M22.0206 16.8198L18.8906 9.49978C18.3206 8.15978 17.4706 7.39978 16.5006 7.34978C15.5406 7.29978 14.6106 7.96978 13.9006 9.24978L12.0006 12.6598C11.6006 13.3798 11.0306 13.8098 10.4106 13.8598C9.78063 13.9198 9.15063 13.5898 8.64063 12.9398L8.42063 12.6598C7.71063 11.7698 6.83063 11.3398 5.93063 11.4298C5.03063 11.5198 4.26063 12.1398 3.75063 13.1498L2.02063 16.5998C1.40063 17.8498 1.46063 19.2998 2.19063 20.4798C2.92063 21.6598 4.19063 22.3698 5.58063 22.3698H18.3406C19.6806 22.3698 20.9306 21.6998 21.6706 20.5798C22.4306 19.4598 22.5506 18.0498 22.0206 16.8198Z" fill="{ postType === 'image' ? 'white' : '#A6ADBB'}" ></path> <path d="M6.96984 8.38012C8.83657 8.38012 10.3498 6.86684 10.3498 5.00012C10.3498 3.13339 8.83657 1.62012 6.96984 1.62012C5.10312 1.62012 3.58984 3.13339 3.58984 5.00012C3.58984 6.86684 5.10312 8.38012 6.96984 8.38012Z" fill="#E5E7EB"></path> </g></svg>-->
|
||||
<div class="flex flex-col items-center mr-5">
|
||||
<label
|
||||
class="tab font-semibold hover:text-gray-200 {postType === 'image'
|
||||
? 'text-gray-200'
|
||||
: 'text-[#9A9996]'}"
|
||||
on:click={() => changePostType("image")}
|
||||
>
|
||||
<span class="text-lg sm:text-2xl">Image/Video</span>
|
||||
</label>
|
||||
<div
|
||||
class="{postType === 'image'
|
||||
? 'bg-[#75D377]'
|
||||
: 'bg-[#09090B]'} h-1 w-[8rem] sm:w-[4rem]"
|
||||
/>
|
||||
</div>
|
||||
<!--<svg class="w-5 h-5 sm:w-6 sm:h-6 inline-block" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="{ postType === 'link' ? 'white' : '#A6ADBB'}"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.975 14.51a1.05 1.05 0 0 0 0-1.485 2.95 2.95 0 0 1 0-4.172l3.536-3.535a2.95 2.95 0 1 1 4.172 4.172l-1.093 1.092a1.05 1.05 0 0 0 1.485 1.485l1.093-1.092a5.05 5.05 0 0 0-7.142-7.142L9.49 7.368a5.05 5.05 0 0 0 0 7.142c.41.41 1.075.41 1.485 0zm2.05-5.02a1.05 1.05 0 0 0 0 1.485 2.95 2.95 0 0 1 0 4.172l-3.5 3.5a2.95 2.95 0 1 1-4.171-4.172l1.025-1.025a1.05 1.05 0 0 0-1.485-1.485L3.87 12.99a5.05 5.05 0 0 0 7.142 7.142l3.5-3.5a5.05 5.05 0 0 0 0-7.142 1.05 1.05 0 0 0-1.485 0z" fill="{ postType === 'link' ? 'white' : '#A6ADBB'}"></path></g></svg>-->
|
||||
<div class="flex flex-col items-center">
|
||||
<label
|
||||
class="tab font-semibold hover:text-gray-200 {postType === 'link'
|
||||
? 'text-gray-200'
|
||||
: 'text-[#9A9996]'}"
|
||||
on:click={() => changePostType("link")}
|
||||
>
|
||||
<span class="text-lg sm:text-2xl">Link</span>
|
||||
</label>
|
||||
<div
|
||||
class="{postType === 'link'
|
||||
? 'bg-[#75D377]'
|
||||
: 'bg-[#09090B]'} h-1 w-[3rem]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
id="title"
|
||||
label=""
|
||||
placeholder="Title"
|
||||
value={form?.data?.title}
|
||||
errors={form?.errors?.title}
|
||||
disabled={loading}
|
||||
maxLength={300}
|
||||
showCounter={true}
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="link"
|
||||
label=""
|
||||
placeholder="Url"
|
||||
value={form?.data?.link}
|
||||
errors={form?.errors?.link}
|
||||
hidden={postType === "link" ? false : true}
|
||||
disabled={loading}
|
||||
useTitle={true}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
id="description"
|
||||
label=""
|
||||
placeholder="Write your Post Content here (optional)"
|
||||
value={form?.data?.description}
|
||||
errors={form?.errors?.description}
|
||||
hidden={postType === "text" ? false : true}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="thumbnail"
|
||||
label=""
|
||||
type="file"
|
||||
errors={form?.errors?.thumbnail}
|
||||
hidden={postType === "image" ? false : true}
|
||||
disabled={loading}
|
||||
/>
|
||||
|
||||
<Tags
|
||||
id="atLeastOneTag"
|
||||
errors={form?.errors?.atLeastOneTag}
|
||||
placeholder={"Please pick at least 1 tag but no more than 3"}
|
||||
/>
|
||||
|
||||
<span
|
||||
class="hidden sm:block text-white text-sm sm:text-[1rem] text-start w-full max-w-2xl"
|
||||
>
|
||||
Use <a
|
||||
href="/markdown-guide"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
class="text-blue-500 sm:hover:text-white"
|
||||
>
|
||||
Markdown
|
||||
</a> to format posts.
|
||||
</span>
|
||||
|
||||
<input type="hidden" name="postType" value={postType} />
|
||||
|
||||
<div class="w-full max-w-sm sm:max-w-2xl pt-5 sm:pt-3">
|
||||
{#if !loading && !isClicked}
|
||||
<button
|
||||
type="submit"
|
||||
class="btn bg-[#fff] sm:hover:bg-gray-300 w-full max-w-2xl normal-case text-lg mb-3"
|
||||
>
|
||||
<span class="text-black">Post</span>
|
||||
</button>
|
||||
{:else}
|
||||
<label
|
||||
class="cursor-not-allowed btn bg-[#fff] w-full max-w-2xl normal-case text-lg mb-3"
|
||||
>
|
||||
<div class="flex flex-row m-auto">
|
||||
<span class="loading loading-infinity"></span>
|
||||
<span class="text-black ml-2">Loading</span>
|
||||
</div>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
<span
|
||||
class="sm:hidden text-white text-sm sm:text-[1rem] text-start w-full max-w-sm sm:max-w-2xl"
|
||||
>
|
||||
Use <a
|
||||
href="/markdown-guide"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
class="text-blue-500 sm:hover:text-white"
|
||||
>
|
||||
Markdown
|
||||
</a> to format posts.
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="hidden xl:inline-block pt-[150px] pl-10">
|
||||
<!-- Sidebar content -->
|
||||
<div class="lg:pl-2 z-20 h-full">
|
||||
<!-- Sidebar content -->
|
||||
|
||||
<!--Community Rules-->
|
||||
<div class="space-y-3 mt-5 fixed sticky" style="top: 5rem;">
|
||||
<div class="bg-[#141417] border border-gray-800 sm:w-96 rounded-lg">
|
||||
<!--Start Header-->
|
||||
<div
|
||||
class="bg-[#141417] border-b border-slate-700 w-full pl-6 pr-6 pt-6 pb-4 rounded-t-lg"
|
||||
>
|
||||
<span class="text-white text-xl ml-1 font-semibold">
|
||||
Community Rules
|
||||
</span>
|
||||
</div>
|
||||
<!--End Header-->
|
||||
|
||||
<!--Start Content-->
|
||||
<div class="w-full pl-7 text-gray-300">
|
||||
<ol class="list-decimal pl-4 pr-4 pb-5 pt-4">
|
||||
<li class="text-[1rem] mb-3">
|
||||
Avoid Purely Policital Discussion
|
||||
</li>
|
||||
<li class="text-[1rem] mb-3">No violence or gory contents</li>
|
||||
<li class="text-[1rem] mb-3">
|
||||
No Advertisement, Self-Promotion, Fundraising, or Begging
|
||||
</li>
|
||||
<li class="text-[1rem]">No illegal activities</li>
|
||||
</ol>
|
||||
</div>
|
||||
<!--End Content-->
|
||||
</div>
|
||||
</div>
|
||||
<!--End Community Rules-->
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
@ -1,228 +0,0 @@
|
||||
import { error, fail, redirect } from "@sveltejs/kit";
|
||||
import { validateData } from "$lib/utils";
|
||||
import { loginUserSchema, registerUserSchema } from "$lib/schemas";
|
||||
|
||||
function listToTree(comments, parentProp = "reply") {
|
||||
// Create id indexed comments dictionary
|
||||
const commentsDict = {};
|
||||
for (let comment of comments) {
|
||||
commentsDict[comment.id] = {
|
||||
...comment,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Build the tree
|
||||
const tree = [];
|
||||
for (const comment of comments) {
|
||||
const parentId = comment[parentProp];
|
||||
if (parentId) {
|
||||
commentsDict[parentId].children.push(commentsDict[comment.id]);
|
||||
} else {
|
||||
tree.push(commentsDict[comment.id]);
|
||||
}
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
export const load = async ({ locals, params, fetch }) => {
|
||||
const { pb } = locals;
|
||||
|
||||
async function getOnePost() {
|
||||
// If the post is not found in the cache, fetch it from the endpoint
|
||||
const postData = { postId: params.postId };
|
||||
|
||||
const response = await fetch("/api/get-one-post", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(postData),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Assuming the result contains an 'items' array
|
||||
return result?.items;
|
||||
}
|
||||
|
||||
const getPostId = async () => {
|
||||
return params.postId;
|
||||
};
|
||||
|
||||
const getModerators = async () => {
|
||||
let output;
|
||||
try {
|
||||
output = await pb.collection("moderators").getFullList({
|
||||
expand: "user",
|
||||
});
|
||||
} catch (e) {
|
||||
output = [];
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
const getAllComments = async () => {
|
||||
let output;
|
||||
try {
|
||||
const result = await pb.collection("comments").getFullList({
|
||||
filter: `post="${params.postId}"`,
|
||||
expand: "user,alreadyVoted(comment)",
|
||||
fields:
|
||||
"*,expand.user,expand.alreadyVoted(comment).user,expand.alreadyVoted(comment).type",
|
||||
sort: "-created",
|
||||
});
|
||||
|
||||
output = listToTree(result);
|
||||
} catch (e) {
|
||||
output = [];
|
||||
}
|
||||
return output;
|
||||
};
|
||||
return {
|
||||
getModerators: await getModerators(),
|
||||
getAllComments: await getAllComments(),
|
||||
getPostId: await getPostId(),
|
||||
getOnePost: await getOnePost(),
|
||||
};
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
login: async ({ request, locals }) => {
|
||||
const { formData, errors } = await validateData(
|
||||
await request.formData(),
|
||||
loginUserSchema
|
||||
);
|
||||
|
||||
if (errors) {
|
||||
return fail(400, {
|
||||
data: formData,
|
||||
errors: errors.fieldErrors,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await locals.pb
|
||||
.collection("users")
|
||||
.authWithPassword(formData.email, formData.password);
|
||||
|
||||
/*
|
||||
if (!locals.pb?.authStore?.model?.verified) {
|
||||
locals.pb.authStore.clear();
|
||||
return {
|
||||
notVerified: true,
|
||||
};
|
||||
}
|
||||
*/
|
||||
} catch (err) {
|
||||
console.log("Error: ", err);
|
||||
error(err.status, err.message);
|
||||
}
|
||||
|
||||
redirect(303, "/community");
|
||||
},
|
||||
|
||||
register: async ({ locals, request }) => {
|
||||
const { formData, errors } = await validateData(
|
||||
await request.formData(),
|
||||
registerUserSchema
|
||||
);
|
||||
|
||||
if (errors) {
|
||||
return fail(400, {
|
||||
data: formData,
|
||||
errors: errors.fieldErrors,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
let newUser = await locals.pb.collection("users").create(formData);
|
||||
/*
|
||||
await locals.pb?.collection('users').update(
|
||||
newUser?.id, {
|
||||
'freeTrial' : true,
|
||||
'tier': 'Pro', //Give new users a free trial for the Pro Subscription
|
||||
});
|
||||
*/
|
||||
await locals.pb.collection("users").requestVerification(formData.email);
|
||||
} catch (err) {
|
||||
console.log("Error: ", err);
|
||||
error(err.status, err.message);
|
||||
}
|
||||
|
||||
try {
|
||||
await locals.pb
|
||||
.collection("users")
|
||||
.authWithPassword(formData.email, formData.password);
|
||||
} catch (err) {
|
||||
console.log("Error: ", err);
|
||||
error(err.status, err.message);
|
||||
}
|
||||
|
||||
redirect(303, "/community");
|
||||
},
|
||||
|
||||
oauth2: async ({ url, locals, request, cookies }) => {
|
||||
const authMethods = await locals?.pb
|
||||
?.collection("users")
|
||||
?.listAuthMethods();
|
||||
|
||||
const data = await request?.formData();
|
||||
const providerSelected = data?.get("provider");
|
||||
|
||||
if (!authMethods) {
|
||||
return {
|
||||
authProviderRedirect: "",
|
||||
authProviderState: "",
|
||||
};
|
||||
}
|
||||
const redirectURL = `${url.origin}/oauth`;
|
||||
|
||||
const targetItem = authMethods.authProviders?.findIndex(
|
||||
(item) => item?.name === providerSelected
|
||||
);
|
||||
//console.log("==================")
|
||||
//console.log(authMethods.authProviders)
|
||||
//console.log('target item is: ', targetItem)
|
||||
|
||||
const provider = authMethods.authProviders[targetItem];
|
||||
const authProviderRedirect = `${provider.authUrl}${redirectURL}`;
|
||||
const state = provider.state;
|
||||
const verifier = provider.codeVerifier;
|
||||
|
||||
cookies.set("state", state, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
path: "/",
|
||||
maxAge: 60 * 60,
|
||||
});
|
||||
|
||||
cookies.set("verifier", verifier, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
path: "/",
|
||||
maxAge: 60 * 60,
|
||||
});
|
||||
|
||||
cookies.set("provider", providerSelected, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
path: "/",
|
||||
maxAge: 60 * 60,
|
||||
});
|
||||
|
||||
cookies.set("path", "/community", {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
path: "/",
|
||||
maxAge: 60,
|
||||
});
|
||||
|
||||
redirect(302, authProviderRedirect);
|
||||
},
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,202 +0,0 @@
|
||||
import { error, fail, redirect } from "@sveltejs/kit";
|
||||
import { validateData } from "$lib/utils";
|
||||
import { loginUserSchema, registerUserSchema } from "$lib/schemas";
|
||||
|
||||
export const load = async ({ locals, params }) => {
|
||||
const { pb, user } = locals;
|
||||
|
||||
if (params.userId === user?.id) {
|
||||
redirect(303, "/community/profile");
|
||||
}
|
||||
|
||||
const userId = async () => {
|
||||
return params.userId;
|
||||
};
|
||||
|
||||
const getModerators = async () => {
|
||||
let output;
|
||||
try {
|
||||
output = await pb.collection("moderators").getFullList({
|
||||
expand: "user",
|
||||
});
|
||||
} catch (e) {
|
||||
output = [];
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
const getUserStats = async () => {
|
||||
let output;
|
||||
|
||||
try {
|
||||
const getNumberOfPosts = await pb.collection("posts").getList(1, 1, {
|
||||
filter: `user="${params.userId}"`,
|
||||
});
|
||||
const numberOfPosts = getNumberOfPosts?.totalItems;
|
||||
|
||||
const getNumberOfComments = await pb
|
||||
.collection("comments")
|
||||
.getList(1, 1, {
|
||||
filter: `user="${params.userId}"`,
|
||||
});
|
||||
const numberOfComments = getNumberOfComments?.totalItems;
|
||||
|
||||
output = { numberOfPosts, numberOfComments };
|
||||
} catch (e) {
|
||||
output = { numberOfPosts: 0, numberOfComments: 0 };
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
const getUserData = async () => {
|
||||
let output;
|
||||
|
||||
try {
|
||||
output = await pb.collection("users").getOne(params?.userId);
|
||||
} catch (e) {
|
||||
output = {
|
||||
karma: 0,
|
||||
username: "-",
|
||||
};
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
return {
|
||||
userId: await userId(),
|
||||
getModerators: await getModerators(),
|
||||
getUserStats: await getUserStats(),
|
||||
getUserData: await getUserData(),
|
||||
};
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
login: async ({ request, locals }) => {
|
||||
const { formData, errors } = await validateData(
|
||||
await request.formData(),
|
||||
loginUserSchema
|
||||
);
|
||||
|
||||
if (errors) {
|
||||
return fail(400, {
|
||||
data: formData,
|
||||
errors: errors.fieldErrors,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await locals.pb
|
||||
.collection("users")
|
||||
.authWithPassword(formData.email, formData.password);
|
||||
|
||||
/*
|
||||
if (!locals.pb?.authStore?.model?.verified) {
|
||||
locals.pb.authStore.clear();
|
||||
return {
|
||||
notVerified: true,
|
||||
};
|
||||
}
|
||||
*/
|
||||
} catch (err) {
|
||||
console.log("Error: ", err);
|
||||
error(err.status, err.message);
|
||||
}
|
||||
|
||||
redirect(303, "/");
|
||||
},
|
||||
|
||||
register: async ({ locals, request }) => {
|
||||
const { formData, errors } = await validateData(
|
||||
await request.formData(),
|
||||
registerUserSchema
|
||||
);
|
||||
|
||||
if (errors) {
|
||||
return fail(400, {
|
||||
data: formData,
|
||||
errors: errors.fieldErrors,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
let newUser = await locals.pb.collection("users").create(formData);
|
||||
/*
|
||||
await locals.pb?.collection('users').update(
|
||||
newUser?.id, {
|
||||
'freeTrial' : true,
|
||||
'tier': 'Pro', //Give new users a free trial for the Pro Subscription
|
||||
});
|
||||
*/
|
||||
await locals.pb.collection("users").requestVerification(formData.email);
|
||||
} catch (err) {
|
||||
console.log("Error: ", err);
|
||||
error(err.status, err.message);
|
||||
}
|
||||
|
||||
try {
|
||||
await locals.pb
|
||||
.collection("users")
|
||||
.authWithPassword(formData.email, formData.password);
|
||||
} catch (err) {
|
||||
console.log("Error: ", err);
|
||||
error(err.status, err.message);
|
||||
}
|
||||
|
||||
redirect(303, "/");
|
||||
},
|
||||
|
||||
oauth2: async ({ url, locals, request, cookies }) => {
|
||||
const authMethods = await pb?.collection("users")?.listAuthMethods();
|
||||
|
||||
const data = await request?.formData();
|
||||
const providerSelected = data?.get("provider");
|
||||
|
||||
if (!authMethods) {
|
||||
return {
|
||||
authProviderRedirect: "",
|
||||
authProviderState: "",
|
||||
};
|
||||
}
|
||||
const redirectURL = `${url.origin}/oauth`;
|
||||
|
||||
const targetItem = authMethods.authProviders?.findIndex(
|
||||
(item) => item?.name === providerSelected
|
||||
);
|
||||
//console.log("==================")
|
||||
//console.log(authMethods.authProviders)
|
||||
//console.log('target item is: ', targetItem)
|
||||
|
||||
const provider = authMethods.authProviders[targetItem];
|
||||
const authProviderRedirect = `${provider.authUrl}${redirectURL}`;
|
||||
const state = provider.state;
|
||||
const verifier = provider.codeVerifier;
|
||||
|
||||
cookies.set("state", state, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
path: "/",
|
||||
maxAge: 60 * 60,
|
||||
});
|
||||
|
||||
cookies.set("verifier", verifier, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
path: "/",
|
||||
maxAge: 60 * 60,
|
||||
});
|
||||
|
||||
cookies.set("provider", providerSelected, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
path: "/",
|
||||
maxAge: 60 * 60,
|
||||
});
|
||||
|
||||
redirect(302, authProviderRedirect);
|
||||
},
|
||||
};
|
||||
@ -1,474 +0,0 @@
|
||||
<script lang="ts">
|
||||
import PostSection from "$lib/components/PostSection.svelte";
|
||||
import SkeletonLoading from "$lib/components/SkeletonLoading.svelte";
|
||||
|
||||
import { onMount } from "svelte";
|
||||
import { getImageURL } from "$lib/utils";
|
||||
import { numberOfUnreadNotification } from "$lib/store";
|
||||
import defaultAvatar from "$lib/images/default_avatar.png";
|
||||
|
||||
import InfiniteLoading from "$lib/components/InfiniteLoading.svelte";
|
||||
|
||||
export let data;
|
||||
export let form;
|
||||
|
||||
let userData = data?.getUserData;
|
||||
|
||||
let userStats = data?.getUserStats ?? {
|
||||
numberOfPosts: 0,
|
||||
numberOfComments: 0,
|
||||
};
|
||||
|
||||
let moderators;
|
||||
let showTab = "post";
|
||||
let isLoaded = false;
|
||||
|
||||
let posts: any[] = [];
|
||||
|
||||
let currentPage = 1;
|
||||
let postLoading = false;
|
||||
let seenPostId = [];
|
||||
|
||||
let noPostMore = false;
|
||||
|
||||
async function infiniteHandler({ detail: { loaded, complete } }) {
|
||||
// console.log("Page position:", window.pageYOffset);
|
||||
seenPostId = posts.map((obj) => obj.id);
|
||||
|
||||
if (!postLoading && !noPostMore) {
|
||||
postLoading = true;
|
||||
|
||||
const newPosts = await getPost();
|
||||
if (newPosts.length === 0) {
|
||||
noPostMore = true;
|
||||
complete();
|
||||
} else {
|
||||
// Remove new posts with duplicate IDs
|
||||
const filteredNewPosts = newPosts.filter(
|
||||
(newPost) => !posts.find((post) => post.id === newPost.id),
|
||||
);
|
||||
posts = [...posts, ...filteredNewPosts];
|
||||
currentPage++;
|
||||
loaded();
|
||||
}
|
||||
postLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function isModerator(moderators) {
|
||||
return moderators?.some(
|
||||
(moderator) => data?.userId === moderator?.expand?.user?.id,
|
||||
);
|
||||
}
|
||||
|
||||
async function getPost() {
|
||||
const postData = {
|
||||
startPage: currentPage,
|
||||
seenPostId: seenPostId.length === 0 ? [] : seenPostId,
|
||||
userId: data?.userId,
|
||||
path: "get-post",
|
||||
};
|
||||
|
||||
// Make the POST request to the endpoint
|
||||
const response = await fetch("/api/fastify-post-data", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(postData),
|
||||
});
|
||||
|
||||
const output = await response.json();
|
||||
|
||||
return output?.items;
|
||||
}
|
||||
|
||||
let loading = true;
|
||||
let LoginPopup;
|
||||
|
||||
onMount(async () => {
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
[posts] = await Promise?.all([getPost()]);
|
||||
|
||||
if (!data?.userData) {
|
||||
LoginPopup = (await import("$lib/components/LoginPopup.svelte")).default;
|
||||
}
|
||||
|
||||
loading = false;
|
||||
isLoaded = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{$numberOfUnreadNotification > 0 ? `(${$numberOfUnreadNotification})` : ""}
|
||||
{userData?.username} · stocknear</title
|
||||
>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="Explore {userData?.username}'s latest posts, comments, and notebooks on stocknear. Discover new insights and connect with other users in the stocknear community."
|
||||
/>
|
||||
<!-- Other meta tags -->
|
||||
<meta property="og:title" content="{userData?.username} · stocknear" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Explore {userData?.username}'s latest posts, comments, and notebooks on stocknear. Discover new insights and connect with other users in the stocknear community."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<!-- Add more Open Graph meta tags as needed -->
|
||||
|
||||
<!-- Twitter specific meta tags -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="{userData?.username} · stocknear" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Explore {userData?.username}'s latest posts, comments, and notebooks on stocknear. Discover new insights and connect with other users in the stocknear community."
|
||||
/>
|
||||
<!-- Add more Twitter meta tags as needed -->
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<body
|
||||
class="bg-[#09090B] max-w-3xl sm:max-w-screen-2xl overflow-hidden mt-10 sm:mt-5"
|
||||
>
|
||||
<!-- Page wrapper -->
|
||||
<div class="flex flex-col min-h-screen overflow-hidden">
|
||||
<main class="grow">
|
||||
<!-- Page content -->
|
||||
<section>
|
||||
<div class="w-full max-w-7xl m-auto sm:px-20 ml-auto">
|
||||
<div class="w-full flex flex-row">
|
||||
<!--Start Profile Pic-->
|
||||
<div
|
||||
class="flex items-center justify-start mb-5 w-screen sm:w-full bg-[#141417] h-48 sm:rounded-xl border border-gray-700 sm:hover:border-gray-600"
|
||||
>
|
||||
<label
|
||||
class="ml-5 avatar w-20 h-20 sm:w-24 sm:h-24 rounded-full"
|
||||
>
|
||||
<img
|
||||
style="clip-path: circle(50%);"
|
||||
class="w-24 bg-slate-300 border border-slate-400 rounded-full inline-block"
|
||||
src={userData?.avatar
|
||||
? getImageURL(
|
||||
userData?.collectionId,
|
||||
userData?.id,
|
||||
userData?.avatar,
|
||||
)
|
||||
: defaultAvatar}
|
||||
alt="User avatar"
|
||||
id="avatar-preview"
|
||||
/>
|
||||
</label>
|
||||
<button id="submit-btn" class="hidden" type="submit"></button>
|
||||
|
||||
<div class="mt-5 ml-5 p-2">
|
||||
<p class="text-sm sm:text-xl text-gray-200 font-semibold">
|
||||
@{userData?.username}
|
||||
|
||||
{#if isLoaded}
|
||||
{#if isModerator(moderators)}
|
||||
<svg
|
||||
class="inline-block w-5 h-5 -ml-0.5 mr-0.5 mb-0.5"
|
||||
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}
|
||||
</p>
|
||||
<span class="text-sm text-gray-200">
|
||||
Joined on {new Date(
|
||||
userData?.created ?? null,
|
||||
)?.toLocaleString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
daySuffix: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
class="hidden lg:inline-block h-sh lg:w-1/3 lg:pr-2 xl:pr-0"
|
||||
>
|
||||
<div class="lg:pl-5 z-20 h-full">
|
||||
<!-- Sidebar content -->
|
||||
|
||||
<!--Start User Profile -->
|
||||
<div class="space-y-6 ml-4">
|
||||
<div
|
||||
class="rounded-xl bg-[#141417] h-48 w-full border border-gray-700 font-mono"
|
||||
>
|
||||
<!--Start Header-->
|
||||
<div class="ml-2 w-full p-3">
|
||||
<span class="text-white text-lg font-medium ml-0.5"
|
||||
>User Profile</span
|
||||
>
|
||||
</div>
|
||||
<hr class="border-b border-gray-700" />
|
||||
<!--End Header-->
|
||||
<!--Start Content-->
|
||||
<div class="w-full p-2">
|
||||
<table
|
||||
class="font-semibold table table-compact bg-[#141417] text-start flex justify-start items-center w-full px-3 m-auto"
|
||||
>
|
||||
<tbody class="bg-[#141417]">
|
||||
<!-- row 1 -->
|
||||
<tr class="text-gray-300">
|
||||
<td class="bg-[#141417] border-b border-[#27272A]"
|
||||
>Karma: {userData?.karma}</td
|
||||
>
|
||||
<td class="bg-[#27272A border-b border-[#27272A]"
|
||||
>Posts: {userStats?.numberOfPosts}</td
|
||||
>
|
||||
</tr>
|
||||
<!-- row 2 -->
|
||||
<tr class="text-gray-300">
|
||||
<td class="bg-[#141417]"
|
||||
>Comments: {userStats?.numberOfComments}</td
|
||||
>
|
||||
<td class="bg-[#141417]"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--End User Profile -->
|
||||
|
||||
{#if userData?.tier === "Pro"}
|
||||
<!--Start Badge-->
|
||||
<div
|
||||
class="rounded-xl bg-[#141417] h-48 w-full border border-gray-700 mt-14"
|
||||
>
|
||||
<!--Start Header-->
|
||||
<div class="ml-2 w-full p-3">
|
||||
<span class="text-white text-lg font-medium ml-0.5"
|
||||
>Badge</span
|
||||
>
|
||||
</div>
|
||||
<hr class="border-b border-gray-700" />
|
||||
<!--End Header-->
|
||||
<!--Start Content-->
|
||||
<div class="w-full p-2 flex flex-col items-start">
|
||||
<div
|
||||
class="ml-2 mt-3 rounded-full border border-gray-500 w-16 h-16 relative bg-[#20202E] flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
style="clip-path: circle(50%);"
|
||||
class="rounded-full w-10"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
><path
|
||||
fill="#F9784E"
|
||||
d="M273.857 21.904c-24.193.012-51.198 5.552-81.1 17.467c143.7 12.608 150.35 129.263 84.032 132.814c-85.27 4.565-53.232-57.217-133.34-103.03C200.445 201.48 94.44 190.33 21.054 59.23c12.805 85.755 24.28 116.942 78.26 153.596C261.996 323.294 94.618 347.8 36.82 245.53c14.568 93.454 68.364 132.803 131.707 139.93c-42.753 24.49-99.452 32.49-143.01 25.556c51.025 42.317 131.606 40.94 193.515 8.576c-37.137 36.123-97.446 70.644-116.803 74.728H276.36C517 405.563 530.305 232.45 454.827 124.492c-2.433 26.21-10.08 49.507-25.545 70.23c-18.48-102.394-69.02-172.86-155.426-172.818zm2.82 184.666l141.384 52.155c.286-3.207.86-6.495 1.747-9.807c5.62-20.973 21.605-34.913 35.705-31.135s20.973 23.842 15.353 44.815c-5.62 20.974-21.603 34.914-35.703 31.136a18 18 0 0 1-2.113-.72l-60.58 49.394l70.637 19.584l-140.023 84.71l65.848-68.866l-31.32-7.006l-150.335 122.58l158.06-196.89l-137.39-41.137l137.006 5.654l-68.275-54.467z"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
<span
|
||||
class="ml-7 mt-2 text-white font-mono font-medium"
|
||||
>Pro</span
|
||||
>
|
||||
</div>
|
||||
<!--End Content-->
|
||||
</div>
|
||||
<!--End Badge-->
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="flex flex-col items-start {userData?.tier ===
|
||||
'Pro'
|
||||
? 'mt-3'
|
||||
: 'mt-12'} ml-2 font-sans"
|
||||
>
|
||||
<div class="flex flex-row gap-x-3">
|
||||
<a
|
||||
href="/about"
|
||||
class="text-sm text-gray-400 hover:text-gray-300 hover:underline"
|
||||
>
|
||||
About
|
||||
</a>
|
||||
<a
|
||||
href="/terms-of-use"
|
||||
class="text-sm text-gray-400 hover:text-gray-300 hover:underline"
|
||||
>
|
||||
Terms
|
||||
</a>
|
||||
<a
|
||||
href="/privacy-policy"
|
||||
class="text-sm text-gray-400 hover:text-gray-300 hover:underline"
|
||||
>
|
||||
Privacy
|
||||
</a>
|
||||
<a
|
||||
href="/contact"
|
||||
class="text-sm text-gray-400 hover:text-gray-300 hover:underline"
|
||||
>
|
||||
Contact
|
||||
</a>
|
||||
<a
|
||||
href="/imprint"
|
||||
class="text-sm text-gray-400 hover:text-gray-300 hover:underline"
|
||||
>
|
||||
Imprint
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<span class="text-sm text-gray-400 mt-1.5">
|
||||
© 2024 stocknear
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!--End Profile Pic-->
|
||||
|
||||
<div class=" w-full">
|
||||
<ul
|
||||
class="w-full font-medium flex flex-row items-center bg-[#09090B] space-x-5 rtl:space-x-reverse py-2"
|
||||
>
|
||||
<li class="cursor-pointer flex flex-col items-center">
|
||||
<label
|
||||
class="cursor-pointer px-3 text-sm sm:text-[0.9rem] font-medium text-gray-400 sm:hover:text-white {showTab ===
|
||||
'post'
|
||||
? 'text-white '
|
||||
: 'bg-[#09090B]'}"
|
||||
>
|
||||
Posts
|
||||
</label>
|
||||
<div
|
||||
class="{showTab === 'post'
|
||||
? 'bg-[#75D377]'
|
||||
: 'bg-[#09090B]'} mt-1 h-[3px] rounded-full w-[2.6rem]"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="md:flex md:justify-between">
|
||||
<!-- Main content -->
|
||||
|
||||
<div class="md:grow pb-12 md:pb-20">
|
||||
<div class="md:pr-6 lg:pr-10 mt-6">
|
||||
{#if showTab === "post"}
|
||||
<div class={!loading ? "hidden" : ""}>
|
||||
{#each Array(5) as _}
|
||||
<SkeletonLoading />
|
||||
{/each}
|
||||
</div>
|
||||
{#if posts?.length === 0}
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<p
|
||||
class="text-center text-gray-400 text-md sm:text-xl mt-10"
|
||||
>
|
||||
You didn't post anything yet
|
||||
</p>
|
||||
<p
|
||||
class="text-center text-gray-400 text-md sm:text-xl mt-3"
|
||||
>
|
||||
Contribute to the community and make your first post
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
{#each posts as post, index}
|
||||
<div class="flex items-start w-full">
|
||||
<div class="w-full m-auto">
|
||||
<PostSection {data} posts={post} {moderators} />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<InfiniteLoading on:infinite={infiniteHandler} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
class="hidden {showTab === 'post'
|
||||
? 'lg:inline-block'
|
||||
: 'hidden'} h-sh w-[300px] pt-[1.5rem]"
|
||||
>
|
||||
<div class="lg:pl-5 z-20 h-full invisible">
|
||||
<!-- Sidebar content -->
|
||||
|
||||
<!--Start User Profile -->
|
||||
<div class="space-y-6">
|
||||
<div
|
||||
class="shadow-lg rounded-md bg-[#09090B] h-auto w-full md:w-80 border border-gray-700"
|
||||
>
|
||||
<!--Start Header-->
|
||||
<div class="bg-[#09090B] w-full p-3">
|
||||
<svg
|
||||
style="clip-path: circle(50%);"
|
||||
class="flex-shrink-0 w-10 h-10 inline-block"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 48 48"
|
||||
><g fill="white"
|
||||
><path
|
||||
d="M32 20a8 8 0 1 1-16 0a8 8 0 0 1 16 0"
|
||||
/><path
|
||||
fill-rule="evenodd"
|
||||
d="M23.184 43.984C12.517 43.556 4 34.772 4 24C4 12.954 12.954 4 24 4s20 8.954 20 20s-8.954 20-20 20h-.274q-.272 0-.542-.016M11.166 36.62a3.028 3.028 0 0 1 2.523-4.005c7.796-.863 12.874-.785 20.632.018a2.99 2.99 0 0 1 2.498 4.002A17.94 17.94 0 0 0 42 24c0-9.941-8.059-18-18-18S6 14.059 6 24c0 4.916 1.971 9.373 5.166 12.621"
|
||||
clip-rule="evenodd"
|
||||
/></g
|
||||
></svg
|
||||
>
|
||||
<span class="text-white text-md ml-0.5"
|
||||
>User Profile</span
|
||||
>
|
||||
</div>
|
||||
<!--End Header-->
|
||||
<!--Start Content-->
|
||||
<div class="w-full p-2 flex-1 flex flex-wrap">
|
||||
<table
|
||||
class="table table-compact bg-[#09090B] text-start flex justify-start items-center w-full px-3 m-auto"
|
||||
>
|
||||
<tbody class="bg-[#09090B]">
|
||||
<!-- row 1 -->
|
||||
<tr class="text-gray-300">
|
||||
<td class="bg-[#09090B] border-b border-[#27272A]"
|
||||
>Karma: {userData?.karma}</td
|
||||
>
|
||||
<td class="bg-[#27272A border-b border-[#27272A]"
|
||||
>Posts: {userStats?.numberOfPosts}</td
|
||||
>
|
||||
</tr>
|
||||
<!-- row 2 -->
|
||||
<tr class="text-gray-300">
|
||||
<td class="bg-[#09090B]"
|
||||
>Comments: {userStats?.numberOfComments}</td
|
||||
>
|
||||
<td class="bg-[#09090B]"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--End User Profile -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</section>
|
||||
|
||||
<!--Start Login Modal-->
|
||||
{#if LoginPopup}
|
||||
<LoginPopup {form} />
|
||||
{/if}
|
||||
<!--End Login Modal-->
|
||||
@ -121,7 +121,6 @@
|
||||
<div class="flex flex-row items-center w-full">
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<a
|
||||
href={"/community/user/" + item?.expand?.user?.id}
|
||||
class="avatar w-11 h-11 flex-shrink-0 cursor-pointer mr-4"
|
||||
>
|
||||
<img
|
||||
@ -139,24 +138,7 @@
|
||||
</a>
|
||||
|
||||
<div class="text-white text-sm sm:text-[1rem]">
|
||||
{#if item?.notifyType === "vote"}
|
||||
<div class="flex flex-col items-start">
|
||||
<div>
|
||||
<a
|
||||
href={"/community/user/" + item?.expand?.user?.id}
|
||||
class="sm:hover:text-white text-blue-400 cursor-pointer"
|
||||
>
|
||||
{item?.expand?.user?.username}
|
||||
</a>
|
||||
<span class="text-white text-sm sm:text-[1rem]">
|
||||
upvoted your {item?.comment ? "comment" : "post"}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-sm sm:text-[1rem] text-[#A6ADBB0"
|
||||
>{formatDate(item?.created)} ago</span
|
||||
>
|
||||
</div>
|
||||
{:else if item?.notifyType === "priceAlert"}
|
||||
{#if item?.notifyType === "priceAlert"}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<div class="flex flex-col items-start">
|
||||
@ -186,27 +168,6 @@
|
||||
>{formatDate(item?.created)} ago</span
|
||||
>
|
||||
</div>
|
||||
{:else if item?.notifyType === "comment"}
|
||||
<div class="flex flex-col items-start">
|
||||
<div>
|
||||
<a
|
||||
href={"/community/user/" + item?.expand?.user?.id}
|
||||
class="sm:hover:text-white text-blue-400 cursor-pointer"
|
||||
>
|
||||
{item?.expand?.user?.username}
|
||||
</a>
|
||||
<span class="text-white text-sm sm:text-[1rem]">
|
||||
commented on your post:
|
||||
</span>
|
||||
{@html item?.expand?.comment?.comment?.length > 30
|
||||
? item?.expand?.comment?.comment?.slice(0, 30) +
|
||||
"..."
|
||||
: item?.expand?.comment?.comment}
|
||||
</div>
|
||||
<span class="text-sm sm:text-[1rem] text-[#A6ADBB0"
|
||||
>{formatDate(item?.created)} ago</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!--
|
||||
|
||||
@ -20,46 +20,9 @@ export const load = async ({ locals }) => {
|
||||
return output;
|
||||
};
|
||||
|
||||
const getModerators = async () => {
|
||||
let output;
|
||||
try {
|
||||
output = await pb.collection("moderators").getFullList({
|
||||
expand: "user",
|
||||
});
|
||||
} catch (e) {
|
||||
output = [];
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
const getUserStats = async () => {
|
||||
let output;
|
||||
|
||||
try {
|
||||
const getNumberOfPosts = await pb.collection("posts").getList(1, 1, {
|
||||
filter: `user="${user?.id}"`,
|
||||
});
|
||||
const numberOfPosts = getNumberOfPosts?.totalItems;
|
||||
|
||||
const getNumberOfComments = await pb
|
||||
.collection("comments")
|
||||
.getList(1, 1, {
|
||||
filter: `user="${user?.id}"`,
|
||||
});
|
||||
const numberOfComments = getNumberOfComments?.totalItems;
|
||||
|
||||
output = { numberOfPosts, numberOfComments };
|
||||
} catch (e) {
|
||||
output = { numberOfPosts: 0, numberOfComments: 0 };
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
return {
|
||||
getSubscriptionData: await getSubscriptionData(),
|
||||
getModerators: await getModerators(),
|
||||
getUserStats: await getUserStats(),
|
||||
};
|
||||
};
|
||||
|
||||
@ -87,7 +50,7 @@ export const actions = {
|
||||
error(err.status, err.message);
|
||||
}
|
||||
|
||||
redirect(302, "/community/profile");
|
||||
redirect(302, "/profile");
|
||||
},
|
||||
|
||||
reactivateSubscription: async ({ request, locals }) => {
|
||||
@ -124,7 +87,7 @@ export const actions = {
|
||||
error(err.status, err.message);
|
||||
}
|
||||
|
||||
redirect(302, "/community/profile");
|
||||
redirect(302, "/profile");
|
||||
},
|
||||
|
||||
changeSubscription: async ({ request, locals }) => {
|
||||
@ -164,6 +127,6 @@ export const actions = {
|
||||
error(err.status, err.message);
|
||||
}
|
||||
|
||||
redirect(302, "/community/profile");
|
||||
redirect(302, "/profile");
|
||||
},
|
||||
};
|
||||
528
src/routes/profile/+page.svelte
Normal file
528
src/routes/profile/+page.svelte
Normal file
@ -0,0 +1,528 @@
|
||||
<script lang="ts">
|
||||
import { numberOfUnreadNotification } from "$lib/store";
|
||||
|
||||
import toast from "svelte-french-toast";
|
||||
|
||||
import { enhance } from "$app/forms";
|
||||
|
||||
export let data;
|
||||
export let form;
|
||||
|
||||
let subscriptionData = data?.getSubscriptionData;
|
||||
let isClicked = false;
|
||||
|
||||
const submitCancellation = () => {
|
||||
return async ({ result, update }) => {
|
||||
switch (result.type) {
|
||||
case "success":
|
||||
toast.success("Subscription Cancelled successfully!", {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
await update();
|
||||
break;
|
||||
case "redirect":
|
||||
toast.success("Subscription Cancelled successfully!", {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
await update();
|
||||
break;
|
||||
case "failure":
|
||||
toast.error("Something went wrong.", {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
await update();
|
||||
break;
|
||||
case "error":
|
||||
toast.error(result.error.message, {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
break;
|
||||
default:
|
||||
await update();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (result.type === "redirect") {
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = "/profile";
|
||||
anchor.dataset.sveltekitReload = true;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.dispatchEvent(new MouseEvent("click"));
|
||||
}
|
||||
}, 1500);
|
||||
};
|
||||
};
|
||||
|
||||
const submitReactivate = () => {
|
||||
return async ({ result, update }) => {
|
||||
switch (result.type) {
|
||||
case "success":
|
||||
toast.success("Subscription Reactivate successfully!", {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
await update();
|
||||
break;
|
||||
case "redirect":
|
||||
toast.success("Subscription Reactivate successfully!", {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
await update();
|
||||
break;
|
||||
case "failure":
|
||||
toast.error("Something went wrong.", {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
await update();
|
||||
break;
|
||||
case "error":
|
||||
toast.error(result.error.message, {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
break;
|
||||
default:
|
||||
await update();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (result.type === "redirect") {
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = "/profile";
|
||||
anchor.dataset.sveltekitReload = true;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.dispatchEvent(new MouseEvent("click"));
|
||||
}
|
||||
}, 1500);
|
||||
};
|
||||
};
|
||||
|
||||
const submitChangePlan = () => {
|
||||
return async ({ result, update }) => {
|
||||
switch (result.type) {
|
||||
case "success":
|
||||
toast.success("Changing to Annual Plan successfully!", {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
await update();
|
||||
break;
|
||||
case "redirect":
|
||||
toast.success("Changing to Annual Plan successfully!", {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
await update();
|
||||
break;
|
||||
case "failure":
|
||||
toast.error("Something went wrong.", {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
await update();
|
||||
break;
|
||||
case "error":
|
||||
toast.error(result.error.message, {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
break;
|
||||
default:
|
||||
await update();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (result.type === "redirect") {
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = "/profile";
|
||||
anchor.dataset.sveltekitReload = true;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.dispatchEvent(new MouseEvent("click"));
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>
|
||||
{$numberOfUnreadNotification > 0 ? `(${$numberOfUnreadNotification})` : ""}
|
||||
My Account · stocknear</title
|
||||
>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
||||
<!-- Other meta tags -->
|
||||
<meta property="og:title" content="My Account · stocknear" />
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<!-- Add more Open Graph meta tags as needed -->
|
||||
|
||||
<!-- Twitter specific meta tags -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="My Account · stocknear" />
|
||||
|
||||
<!-- Add more Twitter meta tags as needed -->
|
||||
</svelte:head>
|
||||
|
||||
<section
|
||||
class="w-full max-w-3xl sm:max-w-screen-2xl overflow-hidden min-h-screen pb-20 pt-5 px-4 lg:px-3"
|
||||
>
|
||||
<div class="text-sm sm:text-[1rem] breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="/" class="text-gray-300">Home</a></li>
|
||||
<li class="text-gray-300">My Account</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="w-full overflow-hidden m-auto mt-5">
|
||||
<div class="sm:p-0 flex justify-center w-full m-auto overflow-hidden">
|
||||
<div
|
||||
class="relative flex justify-center items-start overflow-hidden w-full"
|
||||
>
|
||||
<main class="w-full lg:w-3/4 lg:mr-auto">
|
||||
<div class="mb-6 border-b-[2px]">
|
||||
<h1 class="mb-1 text-white text-2xl sm:text-3xl font-bold">
|
||||
My Account
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-md border border-gray-600 p-4 text-base xs:p-4 xs:text-lg text-white"
|
||||
>
|
||||
<h2 class="text-white text-2xl font-semibold mb-3">
|
||||
User Information
|
||||
</h2>
|
||||
<div class="mt-1">
|
||||
<strong>Email:</strong>
|
||||
{data?.user?.email}
|
||||
</div>
|
||||
<div class="mt-1 mb-1">
|
||||
<strong>Registered Date:</strong>
|
||||
{new Date(data?.user?.created ?? null)?.toLocaleString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
</div>
|
||||
<a href="/update-password" class="sm:hover:text-white text-blue-400"
|
||||
>Update Password</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-6 rounded-md border border-gray-600 p-4 text-base xs:p-4 xs:text-lg text-white"
|
||||
>
|
||||
<h2 class="text-white text-2xl font-semibold mb-3">
|
||||
Manage Subscription
|
||||
</h2>
|
||||
<div class="flex flex-row items-center">
|
||||
<span class="text-white text-[1rem] sm:text-lg"> Status: </span>
|
||||
<div class="ml-2 flex flex-row items-center">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span
|
||||
class="animate-ping absolute inline-flex h-full w-full rounded-full {subscriptionData?.status_formatted ===
|
||||
'Active' ||
|
||||
subscriptionData?.status_formatted === 'Paid' ||
|
||||
subscriptionData?.status_formatted === 'On Trial'
|
||||
? 'bg-[#00FC50]'
|
||||
: 'bg-[#FF3131]'} opacity-75"
|
||||
></span>
|
||||
<span
|
||||
class="relative inline-flex rounded-full h-2 w-2 {subscriptionData?.status_formatted ===
|
||||
'Active' ||
|
||||
subscriptionData?.status_formatted === 'Paid' ||
|
||||
subscriptionData?.status_formatted === 'On Trial'
|
||||
? 'bg-[#00FC50]'
|
||||
: 'bg-[#FF3131]'}"
|
||||
></span>
|
||||
</span>
|
||||
|
||||
<span class="ml-2 text-[1rem] text-slate-200 font-medium">
|
||||
{#if data?.user?.freeTrial === true}
|
||||
Active
|
||||
{:else}
|
||||
{subscriptionData?.status_formatted ?? "Inactive"}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{#if subscriptionData?.status_formatted === "Active"}
|
||||
<span class="text-white text-sm font-medium pr-5">
|
||||
Your subscription will automatically renew on {new Date(
|
||||
subscriptionData?.renews_at,
|
||||
)?.toLocaleDateString("en-GB", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
{:else if subscriptionData?.status_formatted === "Cancelled"}
|
||||
<span class="text-white text-sm font-medium">
|
||||
Your subscription will remain active until {new Date(
|
||||
subscriptionData?.ends_at,
|
||||
)?.toLocaleDateString("en-GB", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
{/if}
|
||||
<div class="flex flex-col justify-start items-start mt-4">
|
||||
<span class="text-white font-medium mr-2 text-lg">
|
||||
Current Plan:
|
||||
</span>
|
||||
<span class="text-[1rem]">
|
||||
{#if subscriptionData?.first_order_item?.product_name === "Pro Subscription (Life Time Access)"}
|
||||
Lifetime Access
|
||||
{:else}
|
||||
{["Active", "Paid", "Cancelled"]?.includes(
|
||||
subscriptionData?.status_formatted,
|
||||
)
|
||||
? subscriptionData?.product_name
|
||||
: "Free Subscription"}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if subscriptionData?.status_formatted === "Active"}
|
||||
<div
|
||||
class="flex flex-col items-start sm:flex-row sm:items-center"
|
||||
>
|
||||
<label
|
||||
for="cancelSubscriptionModal"
|
||||
class="cursor-pointer text-white border border-gray-600 sm:hover:bg-[#27272A] bg-opacity-[0.5] text-sm sm:text-[1rem] px-4 py-2 rounded-md mt-5"
|
||||
>
|
||||
Cancel Subscription
|
||||
</label>
|
||||
{#if subscriptionData?.product_name?.includes("Monthly")}
|
||||
<label
|
||||
for={subscriptionData?.card_brand !== null &&
|
||||
subscriptionData?.card_brand?.length !== 0
|
||||
? "changeSubscriptionModal"
|
||||
: "errorSubscriptionModal"}
|
||||
class="sm:ml-3 {subscriptionData?.card_brand !== null &&
|
||||
subscriptionData?.card_brand?.length !== 0
|
||||
? 'cursor-pointer'
|
||||
: 'cursor-not-allowed'} {subscriptionData?.card_brand !==
|
||||
null && subscriptionData?.card_brand?.length !== 0
|
||||
? 'bg-white sm:hover:bg-white/80 text-black font-semibold'
|
||||
: 'bg-gray-600 opacity-[0.8] text-white'} text-sm sm:text-[1rem] px-4 py-2 rounded-md mt-5"
|
||||
>
|
||||
Change to Annual Plan
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if subscriptionData?.status_formatted === "Cancelled"}
|
||||
<label
|
||||
for="reactivateSubscriptionModal"
|
||||
class="cursor-pointer text-white bg-[#fff] sm:hover:bg-gray-300 text-sm sm:text-[1rem] px-4 py-2 rounded-md mt-5"
|
||||
>
|
||||
Reactivate Subscription
|
||||
</label>
|
||||
{:else if subscriptionData?.status_formatted === "Paid" && !subscriptionData?.first_order_item?.product_name === "Pro Subscription (Life Time Access)"}
|
||||
<span class="text-white mt-5">
|
||||
Please wait a moment; you will be updated to Pro in a second.
|
||||
</span>
|
||||
{:else if subscriptionData?.first_order_item?.product_name === "Pro Subscription (Lifetime Access)"}{:else}
|
||||
<a href="/pricing" class="sm:hover:text-white text-blue-400">
|
||||
Get Full Access with Pro Subscription.
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Start Cancel Subscription Modal -->
|
||||
<input type="checkbox" id="cancelSubscriptionModal" class="modal-toggle" />
|
||||
|
||||
<dialog id="cancelSubscriptionModal" class="modal modal-bottom sm:modal-middle">
|
||||
<label
|
||||
for="cancelSubscriptionModal"
|
||||
class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.5]"
|
||||
></label>
|
||||
|
||||
<!-- Desktop modal content -->
|
||||
<form
|
||||
method="POST"
|
||||
action="?/cancelSubscription"
|
||||
use:enhance={submitCancellation}
|
||||
class="modal-box w-full bg-[#09090B] flex flex-col items-center"
|
||||
>
|
||||
<div
|
||||
class="mx-auto mb-8 h-1.5 w-20 flex-shrink-0 rounded-full bg-[#404040]"
|
||||
/>
|
||||
<div class="text-white mb-5 text-center">
|
||||
<h3 class="font-bold text-2xl mb-5">Are you sure?</h3>
|
||||
<span class="text-white text-[1rem] font-normal">
|
||||
You will no longer be charged for this subscription, and at the end of
|
||||
the billing period, your account will transfer to the Free Plan.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
on:click={() => (isClicked = !isClicked)}
|
||||
class="{!isClicked
|
||||
? ''
|
||||
: 'hidden'} cursor-pointer px-7 py-2 mb-5 rounded-full bg-red-600 text-center text-white text-[1rem] font-normal"
|
||||
>
|
||||
Cancel Subscription
|
||||
<input
|
||||
class="hidden"
|
||||
name="subscriptionId"
|
||||
value={subscriptionData?.first_subscription_item?.subscription_id}
|
||||
/>
|
||||
</button>
|
||||
{#if isClicked === true}
|
||||
<label
|
||||
class="cursor-pointer px-7 py-2 mb-5 rounded-full bg-red-600 text-center text-white text-[1rem] font-normal"
|
||||
>
|
||||
<div class="flex flex-row m-auto">
|
||||
<span class="loading loading-infinity"></span>
|
||||
<span class="text-white ml-2">Proceeding</span>
|
||||
</div>
|
||||
</label>
|
||||
{/if}
|
||||
</form>
|
||||
</dialog>
|
||||
<!-- End Cancel Subscription Modal -->
|
||||
|
||||
<!-- Start Reactivate Subscription Modal -->
|
||||
<input type="checkbox" id="reactivateSubscriptionModal" class="modal-toggle" />
|
||||
|
||||
<dialog
|
||||
id="reactivateSubscriptionModal"
|
||||
class="modal modal-bottom sm:modal-middle"
|
||||
>
|
||||
<label
|
||||
for="reactivateSubscriptionModal"
|
||||
class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.5]"
|
||||
></label>
|
||||
|
||||
<!-- Desktop modal content -->
|
||||
<form
|
||||
method="POST"
|
||||
action="?/reactivateSubscription"
|
||||
use:enhance={submitReactivate}
|
||||
class="modal-box w-full bg-[#09090B] flex flex-col items-center"
|
||||
>
|
||||
<div
|
||||
class="mx-auto mb-8 h-1.5 w-20 flex-shrink-0 rounded-full bg-[#404040]"
|
||||
/>
|
||||
<div class="text-white mb-5 text-center">
|
||||
<h3 class="font-bold text-2xl mb-5">Reactivate Subscription</h3>
|
||||
<span class="text-white text-[1rem] font-normal">
|
||||
Reactivate your Pro Subscription now to unlock unlimited features and
|
||||
gain the edge over the competition.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
on:click={() => (isClicked = !isClicked)}
|
||||
class="{!isClicked
|
||||
? ''
|
||||
: 'hidden'} cursor-pointer px-7 py-2 mb-5 rounded-full bg-[#417143] text-center text-white text-[1rem] font-normal"
|
||||
>
|
||||
Reactivate Subscription
|
||||
<input
|
||||
class="hidden"
|
||||
name="subscriptionId"
|
||||
value={subscriptionData?.first_subscription_item?.subscription_id}
|
||||
/>
|
||||
</button>
|
||||
{#if isClicked === true}
|
||||
<label
|
||||
class="cursor-pointer px-7 py-2 mb-5 rounded-full bg-[#417143] text-center text-white text-[1rem] font-normal"
|
||||
>
|
||||
<div class="flex flex-row m-auto">
|
||||
<span class="loading loading-infinity"></span>
|
||||
<span class="text-white ml-2">Proceeding</span>
|
||||
</div>
|
||||
</label>
|
||||
{/if}
|
||||
</form>
|
||||
</dialog>
|
||||
<!-- End Reactivate Subscription Modal -->
|
||||
|
||||
<!-- Start Cancel Subscription Modal -->
|
||||
<input type="checkbox" id="changeSubscriptionModal" class="modal-toggle" />
|
||||
|
||||
<dialog id="changeSubscriptionModal" class="modal modal-bottom sm:modal-middle">
|
||||
<label
|
||||
for="changeSubscriptionModal"
|
||||
class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.5]"
|
||||
></label>
|
||||
|
||||
<!-- Desktop modal content -->
|
||||
<form
|
||||
method="POST"
|
||||
action="?/changeSubscription"
|
||||
use:enhance={submitChangePlan}
|
||||
class="modal-box w-full bg-[#09090B] flex flex-col items-center"
|
||||
>
|
||||
<div
|
||||
class="mx-auto mb-8 h-1.5 w-20 flex-shrink-0 rounded-full bg-[#404040]"
|
||||
/>
|
||||
<div class="text-white mb-5 text-center">
|
||||
<h3 class="font-bold text-2xl mb-5">Are you sure?</h3>
|
||||
<span class="text-white text-[1rem] font-normal">
|
||||
You're account will transfer from from monthly plan to annual plan.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
on:click={() => (isClicked = !isClicked)}
|
||||
class="{!isClicked
|
||||
? ''
|
||||
: 'hidden'} cursor-pointer px-7 py-2 mb-5 rounded-full text-center bg-[#00FC50] text-black font-semibold text-[1rem] font-normal"
|
||||
>
|
||||
Change to Annual Plan
|
||||
<input
|
||||
class="hidden"
|
||||
name="subscriptionId"
|
||||
value={subscriptionData?.first_subscription_item?.subscription_id}
|
||||
/>
|
||||
</button>
|
||||
{#if isClicked === true}
|
||||
<label
|
||||
class="cursor-pointer px-7 py-2 mb-5 rounded-full bg-[#0DDE00] text-center text-black text-[1rem] font-normal"
|
||||
>
|
||||
<div class="flex flex-row m-auto">
|
||||
<span class="loading loading-infinity"></span>
|
||||
<span class="text-black ml-2">Proceeding</span>
|
||||
</div>
|
||||
</label>
|
||||
{/if}
|
||||
</form>
|
||||
</dialog>
|
||||
<!-- End Cancel Subscription Modal -->
|
||||
|
||||
<!-- Start Cancel Subscription Modal -->
|
||||
<input type="checkbox" id="errorSubscriptionModal" class="modal-toggle" />
|
||||
|
||||
<dialog id="errorSubscriptionModal" class="modal modal-bottom sm:modal-middle">
|
||||
<label
|
||||
for="errorSubscriptionModal"
|
||||
class="cursor-pointer modal-backdrop bg-[#000] bg-opacity-[0.5]"
|
||||
></label>
|
||||
|
||||
<!-- Desktop modal content -->
|
||||
<div class="modal-box w-full bg-[#09090B] flex flex-col items-center">
|
||||
<div
|
||||
class="mx-auto mb-8 h-1.5 w-20 flex-shrink-0 rounded-full bg-[#404040]"
|
||||
/>
|
||||
<div class="text-white mb-5 text-center">
|
||||
<h3 class="font-bold text-2xl mb-5">Paypal not supported</h3>
|
||||
<span class="text-white text-[1rem] font-normal">
|
||||
Apologies, our payment provider currently only supports credit cards for
|
||||
changing plans from monthly to annual. We are working to expand this to
|
||||
other payment methods.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label
|
||||
for="errorSubscriptionModal"
|
||||
class="cursor-pointer px-7 py-2 mb-5 rounded-full bg-[#0DDE00] text-center text-black text-[1rem] font-normal"
|
||||
>
|
||||
OK
|
||||
</label>
|
||||
</div>
|
||||
</dialog>
|
||||
<!-- End Cancel Subscription Modal -->
|
||||
@ -69,8 +69,6 @@ const pages = [
|
||||
{ title: "/market-mover/active" },
|
||||
{ title: "/market-mover/premarket" },
|
||||
{ title: "/market-mover/afterhours" },
|
||||
{ title: "/community" },
|
||||
{ title: "/community/create-post" },
|
||||
{ title: "/hedge-funds" },
|
||||
{ title: "/login" },
|
||||
{ title: "/register" },
|
||||
@ -99,17 +97,7 @@ export async function GET({ locals }) {
|
||||
//get all posts;
|
||||
const { apiKey, apiURL } = locals;
|
||||
|
||||
const outputPost = await locals.pb.collection("posts").getFullList();
|
||||
|
||||
const outputBlogPost = await locals.pb.collection("articles").getFullList();
|
||||
|
||||
const posts = outputPost?.map((item) => ({
|
||||
id: item.id,
|
||||
}));
|
||||
|
||||
const articles = outputBlogPost?.map((item) => ({
|
||||
id: item.id,
|
||||
}));
|
||||
|
||||
const rawData = await fetch(apiURL + "/searchbar-data", {
|
||||
method: "GET",
|
||||
@ -125,7 +113,7 @@ export async function GET({ locals }) {
|
||||
type: item?.type,
|
||||
}));
|
||||
|
||||
const body = sitemap(posts, articles, stocks, pages);
|
||||
const body = sitemap(stocks, pages);
|
||||
const response = new Response(body);
|
||||
response.headers.set("Cache-Control", "max-age=0, s-maxage=3600");
|
||||
response.headers.set("Content-Type", "application/xml");
|
||||
@ -134,8 +122,6 @@ export async function GET({ locals }) {
|
||||
|
||||
// Modified sitemap function
|
||||
const sitemap = (
|
||||
posts,
|
||||
articles,
|
||||
stocks,
|
||||
pages,
|
||||
) => `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
@ -172,22 +158,4 @@ const sitemap = (
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
${articles
|
||||
.map(
|
||||
(article) => `
|
||||
<url>
|
||||
<loc>${website}/blog/article/${article.id}</loc>
|
||||
</url>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
${posts
|
||||
.map(
|
||||
(post) => `
|
||||
<url>
|
||||
<loc>${website}/community/post/${post.id}</loc>
|
||||
</url>
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</urlset>`;
|
||||
|
||||
8
src/routes/update-password/+page.server.ts
Normal file
8
src/routes/update-password/+page.server.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { redirect, error } from "@sveltejs/kit";
|
||||
|
||||
export const load = async ({ locals }) => {
|
||||
const { pb, user } = locals;
|
||||
if (!pb.authStore.isValid) {
|
||||
redirect(303, "/login");
|
||||
}
|
||||
}
|
||||
120
src/routes/update-password/+page.svelte
Normal file
120
src/routes/update-password/+page.svelte
Normal file
@ -0,0 +1,120 @@
|
||||
<script ts="lang">
|
||||
import { pb } from "$lib/pocketbase";
|
||||
import toast from "svelte-french-toast";
|
||||
import Input from "$lib/components/Input.svelte";
|
||||
import { updatePasswordSchema } from "$lib/schemas";
|
||||
import { goto } from "$app/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
let zodErrors = [];
|
||||
export let data;
|
||||
|
||||
let errorOldPassword = "";
|
||||
let errorPassword = "";
|
||||
let errorPasswordConfirm = "";
|
||||
let loading = false;
|
||||
async function updatePassword(event) {
|
||||
loading = true;
|
||||
event.preventDefault(); // prevent the default form submission behavior
|
||||
errorOldPassword = "";
|
||||
errorPassword = "";
|
||||
errorPasswordConfirm = "";
|
||||
|
||||
const formData = new FormData(event.target); // create a FormData object from the form
|
||||
const postData = {};
|
||||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
postData[key] = value;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use Zod validation
|
||||
const cleanedData = updatePasswordSchema.parse(postData);
|
||||
await pb.collection("users").update(data?.user?.id, cleanedData);
|
||||
toast.success("Password updated!", {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
// Handle Zod validation errors
|
||||
zodErrors = error.errors;
|
||||
|
||||
errorOldPassword =
|
||||
zodErrors?.find((err) => err.path[0] === "oldPassword")?.message ??
|
||||
"";
|
||||
errorPassword =
|
||||
zodErrors?.find((err) => err.path[0] === "password")?.message ?? "";
|
||||
errorPasswordConfirm =
|
||||
zodErrors?.find((err) => err.path[0] === "passwordConfirm")
|
||||
?.message ?? "";
|
||||
|
||||
toast.error("Invalid credentials", {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
} else {
|
||||
// Handle other errors
|
||||
console.error("Unexpected error during registration:", error);
|
||||
|
||||
toast.error("An unexpected error occurred", {
|
||||
style: "border-radius: 200px; background: #333; color: #fff;",
|
||||
});
|
||||
}
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="w-full max-w-3xl sm:max-w-screen-2xl overflow-hidden min-h-screen pb-20 pt-5 px-4 lg:px-3"
|
||||
>
|
||||
<div class="w-full overflow-hidden m-auto mt-5">
|
||||
<div class="sm:p-0 flex justify-center w-full m-auto overflow-hidden">
|
||||
<div
|
||||
class="relative flex justify-center items-start overflow-hidden w-full"
|
||||
>
|
||||
<main class="w-full">
|
||||
<form
|
||||
on:submit={updatePassword}
|
||||
class="flex flex-col space-y-2 w-full max-w-lg m-auto"
|
||||
>
|
||||
<h1
|
||||
class="mb-1 text-white text-2xl sm:text-3xl font-bold mb-6 text-center"
|
||||
>
|
||||
Set a new password
|
||||
</h1>
|
||||
|
||||
<Input
|
||||
id="oldPassword"
|
||||
label="Old Password"
|
||||
type="password"
|
||||
required
|
||||
errors={errorOldPassword}
|
||||
/>
|
||||
<Input
|
||||
id="password"
|
||||
label="New Password"
|
||||
type="password"
|
||||
required
|
||||
errors={errorPassword}
|
||||
/>
|
||||
<Input
|
||||
id="passwordConfirm"
|
||||
label="Confirm New Password"
|
||||
type="password"
|
||||
required
|
||||
errors={errorPasswordConfirm}
|
||||
/>
|
||||
|
||||
<div class="w-full max-w-lg pt-3">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn bg-[#fff] text-black font-semibold sm:hover:bg-gray-300 w-full max-w-lg normal-case text-md"
|
||||
>Update Password</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -458,10 +458,10 @@
|
||||
</span>
|
||||
{#if !data?.user}
|
||||
<a
|
||||
class="w-64 flex mt-5 justify-center items-center m-auto btn text-white border border-gray-600 group"
|
||||
class="w-64 flex mt-10 justify-center items-center m-auto btn text-black bg-[#fff] sm:hover:bg-gray-300 transition duration-150 ease-in-out group"
|
||||
href="/register"
|
||||
>
|
||||
<span>Get Started</span>
|
||||
Get Started
|
||||
<span
|
||||
class="tracking-normal group-hover:translate-x-0.5 transition-transform duration-150 ease-in-out"
|
||||
>
|
||||
@ -474,7 +474,7 @@
|
||||
><path
|
||||
d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z"
|
||||
/><path
|
||||
fill="white"
|
||||
fill="black"
|
||||
d="M13.06 3.283a1.5 1.5 0 0 0-2.12 0L5.281 8.939a1.5 1.5 0 0 0 2.122 2.122L10.5 7.965V19.5a1.5 1.5 0 0 0 3 0V7.965l3.096 3.096a1.5 1.5 0 1 0 2.122-2.122L13.06 3.283Z"
|
||||
/></g
|
||||
></g
|
||||
|
||||
@ -1063,7 +1063,7 @@
|
||||
</span>
|
||||
{#if !data?.user}
|
||||
<a
|
||||
class="w-64 flex mt-10 justify-center items-center m-auto btn text-white bg-[#fff] sm:hover:bg-gray-300 transition duration-150 ease-in-out group"
|
||||
class="w-64 flex mt-10 justify-center items-center m-auto btn text-black bg-[#fff] sm:hover:bg-gray-300 transition duration-150 ease-in-out group"
|
||||
href="/register"
|
||||
>
|
||||
Get Started
|
||||
@ -1079,7 +1079,7 @@
|
||||
><path
|
||||
d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z"
|
||||
/><path
|
||||
fill="white"
|
||||
fill="black"
|
||||
d="M13.06 3.283a1.5 1.5 0 0 0-2.12 0L5.281 8.939a1.5 1.5 0 0 0 2.122 2.122L10.5 7.965V19.5a1.5 1.5 0 0 0 3 0V7.965l3.096 3.096a1.5 1.5 0 1 0 2.122-2.122L13.06 3.283Z"
|
||||
/></g
|
||||
></g
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user