add image gallery slider & emoji in topic singl page and othrs improvment
parent
0634c26138
commit
9f2f235c09
|
@ -26,6 +26,7 @@
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"emoji-picker-react": "^4.12.3",
|
||||||
"framer-motion": "^12.18.1",
|
"framer-motion": "^12.18.1",
|
||||||
"image-resize-compress": "^2.1.1",
|
"image-resize-compress": "^2.1.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
@ -6729,6 +6730,21 @@
|
||||||
"integrity": "sha512-oTUp3gfX1gZI+xfD2djr2rzQdHCwHzPQrrK0CD7WpTdF0nPdQ/INcRVjWgLdCT4a9W3jFObR9DAfsuyFQnI8CQ==",
|
"integrity": "sha512-oTUp3gfX1gZI+xfD2djr2rzQdHCwHzPQrrK0CD7WpTdF0nPdQ/INcRVjWgLdCT4a9W3jFObR9DAfsuyFQnI8CQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/emoji-picker-react": {
|
||||||
|
"version": "4.12.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.12.3.tgz",
|
||||||
|
"integrity": "sha512-Pf+pTenW/uM+Juw197dbCtcB45uqvl9Y5/BzK44L72Aqvavt4UPmCqb+w0UKzUJkzh0Tp1rThfHVwoQXXbOZvQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"flairup": "1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "10.4.0",
|
"version": "10.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
|
||||||
|
@ -7135,6 +7151,12 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/flairup": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/flattie": {
|
"node_modules/flattie": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz",
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"emoji-picker-react": "^4.12.3",
|
||||||
"framer-motion": "^12.18.1",
|
"framer-motion": "^12.18.1",
|
||||||
"image-resize-compress": "^2.1.1",
|
"image-resize-compress": "^2.1.1",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import EmojiPicker from 'emoji-picker-react';
|
||||||
import { Card } from '../ui/card';
|
import { Card } from '../ui/card';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Textarea } from '../ui/textarea';
|
import { Textarea } from '../ui/textarea';
|
||||||
import CommentThread from './CommentThread';
|
import CommentThread from './CommentThread';
|
||||||
import { token, user_name, pb_id, siliconId, isLogin } from '../../lib/CookieValues';
|
import { token, user_name, pb_id, siliconId, isLogin } from '../../lib/CookieValues';
|
||||||
import { useIsLoggedIn } from '../../lib/isLoggedIn';
|
import { useIsLoggedIn } from '../../lib/isLoggedIn';
|
||||||
|
|
||||||
const CommentSystem = ({ topicId, pbId }) => {
|
const CommentSystem = ({ topicId, pbId }) => {
|
||||||
const isLoginCoockie = JSON.parse(isLogin);
|
const isLoginCoockie = JSON.parse(isLogin);
|
||||||
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||||
|
@ -12,16 +14,55 @@ const CommentSystem = ({ topicId, pbId }) => {
|
||||||
const [newComment, setNewComment] = useState('');
|
const [newComment, setNewComment] = useState('');
|
||||||
const [commentsLoading, setCommentsLoading] = useState(false);
|
const [commentsLoading, setCommentsLoading] = useState(false);
|
||||||
const [commentsError, setCommentsError] = useState('');
|
const [commentsError, setCommentsError] = useState('');
|
||||||
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
|
|
||||||
|
const textareaRef = useRef(null);
|
||||||
|
const emojiPickerRef = useRef(null);
|
||||||
|
|
||||||
|
// 📌 Insert emoji at cursor position
|
||||||
|
const insertEmojiAtCursor = (emoji) => {
|
||||||
|
const textarea = textareaRef.current;
|
||||||
|
if (!textarea) return;
|
||||||
|
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const text = newComment;
|
||||||
|
|
||||||
|
const updatedText = text.slice(0, start) + emoji + text.slice(end);
|
||||||
|
setNewComment(updatedText);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
textarea.focus();
|
||||||
|
textarea.setSelectionRange(start + emoji.length, start + emoji.length);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 📌 Outside click to close emoji picker
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e) => {
|
||||||
|
if (
|
||||||
|
emojiPickerRef.current &&
|
||||||
|
!emojiPickerRef.current.contains(e.target) &&
|
||||||
|
!e.target.closest('#emoji-toggle-btn')
|
||||||
|
) {
|
||||||
|
setShowEmojiPicker(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 📌 Comment nesting logic
|
||||||
const nestComments = (flatComments) => {
|
const nestComments = (flatComments) => {
|
||||||
const commentMap = {};
|
const commentMap = {};
|
||||||
const rootComments = [];
|
const rootComments = [];
|
||||||
|
|
||||||
flatComments?.forEach(comment => {
|
flatComments?.forEach(comment => {
|
||||||
comment.replies = [];
|
comment.replies = [];
|
||||||
commentMap[comment.comment_id] = comment;
|
commentMap[comment.comment_id] = comment;
|
||||||
});
|
});
|
||||||
|
|
||||||
flatComments?.forEach(comment => {
|
flatComments?.forEach(comment => {
|
||||||
if (comment.parent_comment_id?.Valid) {
|
if (comment.parent_comment_id?.Valid) {
|
||||||
const parent = commentMap[comment.parent_comment_id.String];
|
const parent = commentMap[comment.parent_comment_id.String];
|
||||||
|
@ -30,59 +71,54 @@ const CommentSystem = ({ topicId, pbId }) => {
|
||||||
rootComments.push(comment);
|
rootComments.push(comment);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return rootComments.sort((a, b) =>
|
return rootComments.sort((a, b) =>
|
||||||
new Date(b.created_at) - new Date(a.created_at));
|
new Date(b.created_at) - new Date(a.created_at));
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchComments = async () => {
|
const fetchComments = async () => {
|
||||||
try {
|
try {
|
||||||
setCommentsLoading(true);
|
setCommentsLoading(true);
|
||||||
setCommentsError('');
|
setCommentsError('');
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({ topic_id: String(topicId) });
|
||||||
topic_id: String(topicId),
|
|
||||||
});
|
if (siliconId && siliconId !== 'null') {
|
||||||
|
params.append('silicon_id', siliconId);
|
||||||
// Only add silicon_id if it has a value
|
}
|
||||||
if (siliconId && siliconId !== 'null') {
|
|
||||||
params.append('silicon_id', siliconId);
|
if (pb_id && pb_id !== 'null') {
|
||||||
}
|
params.append('pb_id', pb_id);
|
||||||
|
}
|
||||||
// Only add pb_id if it has a value
|
|
||||||
if (pb_id && pb_id !== 'null') {
|
const response = await fetch(`https://sp-comments-prod-pocndhvgmcnbacgb.siliconpin.com/comments?${params.toString()}`);
|
||||||
params.append('pb_id', pb_id);
|
|
||||||
}
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
const response = await fetch(`https://sp-comments-prod-pocndhvgmcnbacgb.siliconpin.com/comments?${params.toString()}`);
|
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const flatComments = await response.json();
|
||||||
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
|
setComments(nestComments(flatComments || []));
|
||||||
}
|
|
||||||
|
|
||||||
const flatComments = await response.json();
|
|
||||||
setComments(nestComments(flatComments || []));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setCommentsError(err.message);
|
setCommentsError(err.message);
|
||||||
console.error('Fetch error:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setCommentsLoading(false);
|
setCommentsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const postComment = async (text, parentId = null) => {
|
const postComment = async (text, parentId = null) => {
|
||||||
try {
|
try {
|
||||||
setCommentsLoading(true);
|
setCommentsLoading(true);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
topic_id: String(topicId),
|
topic_id: String(topicId),
|
||||||
silicon_id: siliconId,
|
silicon_id: siliconId,
|
||||||
pb_id: pb_id,
|
pb_id: pbId,
|
||||||
user_id: siliconId,
|
user_id: siliconId,
|
||||||
user_name: user_name,
|
user_name: user_name,
|
||||||
comment_text: text,
|
comment_text: text,
|
||||||
is_approved: true
|
is_approved: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
|
@ -90,11 +126,11 @@ const CommentSystem = ({ topicId, pbId }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = parentId ? '/comments/reply' : '/comments';
|
const endpoint = parentId ? '/comments/reply' : '/comments';
|
||||||
|
|
||||||
const response = await fetch(`https://sp-comments-prod-pocndhvgmcnbacgb.siliconpin.com${endpoint}`, {
|
const response = await fetch(`https://sp-comments-prod-pocndhvgmcnbacgb.siliconpin.com${endpoint}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
@ -115,22 +151,22 @@ const CommentSystem = ({ topicId, pbId }) => {
|
||||||
}, [topicId, siliconId, pbId]);
|
}, [topicId, siliconId, pbId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className=" mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
<div className="mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||||
<h2 className="text-2xl font-bold text-[#6d9e37] mb-6">Comments</h2>
|
<h2 className="text-2xl font-bold text-[#6d9e37] mb-6">Comments</h2>
|
||||||
|
|
||||||
{commentsError && (
|
{commentsError && (
|
||||||
<div className="bg-red-50 text-red-700 p-3 rounded-md mb-4">
|
<div className="bg-red-50 text-red-700 p-3 rounded-md mb-4">
|
||||||
{commentsError}
|
{commentsError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{commentsLoading && comments.length === 0 ? (
|
{commentsLoading && comments.length === 0 ? (
|
||||||
<div className="text-center py-8 text-[#6d9e37]">Loading comments...</div>
|
<div className="text-center py-8 text-[#6d9e37]">Loading comments...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{comments.length > 0 ? (
|
{comments.length > 0 ? (
|
||||||
comments.map(comment => (
|
comments.map(comment => (
|
||||||
<CommentThread key={comment.comment_id} comment={comment} onReply={postComment} />
|
<CommentThread key={comment.comment_id} comment={comment} onReply={postComment} />
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<p className="text-center py-8 text-gray-500 italic">
|
<p className="text-center py-8 text-gray-500 italic">
|
||||||
|
@ -139,19 +175,19 @@ const CommentSystem = ({ topicId, pbId }) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='relative'>
|
|
||||||
{
|
<div className="relative">
|
||||||
isLoginCoockie !== true ? (
|
{isLoginCoockie !== true ? (
|
||||||
<div className="absolute bg-black bg-opacity-70 w-full h-full rounded-lg">
|
<div className="absolute bg-black bg-opacity-70 w-full h-full rounded-lg">
|
||||||
<div className='absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2'>
|
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||||
<p className='italic bg-[#2a2e35] px-2 py-1.5 rounded-md shadow-xl'>To join the conversation, please log in first.</p>
|
<p className="italic bg-[#2a2e35] px-2 py-1.5 rounded-md shadow-xl">
|
||||||
</div>
|
To join the conversation, please log in first.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
''
|
) : null}
|
||||||
)
|
|
||||||
}
|
<Card className="mt-8 p-6 shadow-md border border-gray-200">
|
||||||
<Card className="mt-8 p-6">
|
|
||||||
<h3 className="text-lg font-medium mb-4">Add a Comment</h3>
|
<h3 className="text-lg font-medium mb-4">Add a Comment</h3>
|
||||||
<form onSubmit={(e) => {
|
<form onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -159,9 +195,50 @@ const CommentSystem = ({ topicId, pbId }) => {
|
||||||
postComment(newComment);
|
postComment(newComment);
|
||||||
setNewComment('');
|
setNewComment('');
|
||||||
}}>
|
}}>
|
||||||
<Textarea value={newComment} onChange={(e) => setNewComment(e.target.value)} placeholder="Share your thoughts..." className="min-h-[100px]" required minLength={1}/>
|
<div className="relative">
|
||||||
|
<Textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
value={newComment}
|
||||||
|
onChange={(e) => setNewComment(e.target.value)}
|
||||||
|
placeholder="Share your thoughts..."
|
||||||
|
className="min-h-[100px] pr-12 border rounded-md"
|
||||||
|
required
|
||||||
|
minLength={1}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Emoji Button */}
|
||||||
|
<button
|
||||||
|
id="emoji-toggle-btn"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
||||||
|
className="absolute bottom-3 right-3 text-xl text-gray-500 hover:text-yellow-500 transition-colors"
|
||||||
|
title="Add Emoji"
|
||||||
|
>
|
||||||
|
<svg width="20px" height="20px" 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="M8.5 11C9.32843 11 10 10.3284 10 9.5C10 8.67157 9.32843 8 8.5 8C7.67157 8 7 8.67157 7 9.5C7 10.3284 7.67157 11 8.5 11Z" fill="#6d9e37"></path> <path d="M17 9.5C17 10.3284 16.3284 11 15.5 11C14.6716 11 14 10.3284 14 9.5C14 8.67157 14.6716 8 15.5 8C16.3284 8 17 8.67157 17 9.5Z" fill="#6d9e37"></path> <path d="M8.88875 13.5414C8.63822 13.0559 8.0431 12.8607 7.55301 13.1058C7.05903 13.3528 6.8588 13.9535 7.10579 14.4474C7.18825 14.6118 7.29326 14.7659 7.40334 14.9127C7.58615 15.1565 7.8621 15.4704 8.25052 15.7811C9.04005 16.4127 10.2573 17.0002 12.0002 17.0002C13.7431 17.0002 14.9604 16.4127 15.7499 15.7811C16.1383 15.4704 16.4143 15.1565 16.5971 14.9127C16.7076 14.7654 16.8081 14.6113 16.8941 14.4485C17.1387 13.961 16.9352 13.3497 16.4474 13.1058C15.9573 12.8607 15.3622 13.0559 15.1117 13.5414C15.0979 13.5663 14.9097 13.892 14.5005 14.2194C14.0401 14.5877 13.2573 15.0002 12.0002 15.0002C10.7431 15.0002 9.96038 14.5877 9.49991 14.2194C9.09071 13.892 8.90255 13.5663 8.88875 13.5414Z" fill="#6d9e37"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23ZM12 20.9932C7.03321 20.9932 3.00683 16.9668 3.00683 12C3.00683 7.03321 7.03321 3.00683 12 3.00683C16.9668 3.00683 20.9932 7.03321 20.9932 12C20.9932 16.9668 16.9668 20.9932 12 20.9932Z" fill="#6d9e37"></path> </g></svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Emoji Picker */}
|
||||||
|
{showEmojiPicker && (
|
||||||
|
<div
|
||||||
|
ref={emojiPickerRef}
|
||||||
|
className="absolute bottom-16 right-0 z-50 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<EmojiPicker
|
||||||
|
onEmojiClick={(emojiData) => insertEmojiAtCursor(emojiData.emoji)}
|
||||||
|
height={400}
|
||||||
|
width={300}
|
||||||
|
theme="dark"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<Button type="submit" disabled={commentsLoading || !newComment.trim()} className="w-full sm:w-auto">
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={commentsLoading || !newComment.trim()}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
>
|
||||||
{commentsLoading ? (
|
{commentsLoading ? (
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
@ -180,4 +257,4 @@ const CommentSystem = ({ topicId, pbId }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CommentSystem;
|
export default CommentSystem;
|
||||||
|
|
|
@ -1,12 +1,51 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import EmojiPicker from 'emoji-picker-react';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Textarea } from '../ui/textarea';
|
import { Textarea } from '../ui/textarea';
|
||||||
import { token, user_name, pb_id, siliconId, isLogin } from '../../lib/CookieValues';
|
import { token, user_name, pb_id, siliconId, isLogin } from '../../lib/CookieValues';
|
||||||
|
|
||||||
const CommentThread = ({ comment, depth = 0, onReply }) => {
|
const CommentThread = ({ comment, depth = 0, onReply }) => {
|
||||||
const isLoginCoockie = JSON.parse(isLogin);
|
const isLoginCoockie = JSON.parse(isLogin);
|
||||||
const [isReplying, setIsReplying] = useState(false);
|
const [isReplying, setIsReplying] = useState(false);
|
||||||
const [replyText, setReplyText] = useState('');
|
const [replyText, setReplyText] = useState('');
|
||||||
const [replyError, setReplyError] = useState('')
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
|
|
||||||
|
const textareaRef = useRef(null);
|
||||||
|
const emojiPickerRef = useRef(null);
|
||||||
|
|
||||||
|
// 🧠 Emoji insert at cursor
|
||||||
|
const insertEmojiAtCursor = (emoji) => {
|
||||||
|
const textarea = textareaRef.current;
|
||||||
|
if (!textarea) return;
|
||||||
|
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const text = replyText;
|
||||||
|
|
||||||
|
const updatedText = text.slice(0, start) + emoji + text.slice(end);
|
||||||
|
setReplyText(updatedText);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
textarea.focus();
|
||||||
|
textarea.setSelectionRange(start + emoji.length, start + emoji.length);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🧠 Close emoji picker on outside click
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e) => {
|
||||||
|
if (
|
||||||
|
emojiPickerRef.current &&
|
||||||
|
!emojiPickerRef.current.contains(e.target) &&
|
||||||
|
!e.target.closest(`#reply-emoji-btn-${comment.comment_id}`)
|
||||||
|
) {
|
||||||
|
setShowEmojiPicker(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, [comment.comment_id]);
|
||||||
|
|
||||||
const handleReplySubmit = (e) => {
|
const handleReplySubmit = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -14,6 +53,7 @@ const CommentThread = ({ comment, depth = 0, onReply }) => {
|
||||||
onReply(replyText, comment.comment_id);
|
onReply(replyText, comment.comment_id);
|
||||||
setReplyText('');
|
setReplyText('');
|
||||||
setIsReplying(false);
|
setIsReplying(false);
|
||||||
|
setShowEmojiPicker(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -23,11 +63,10 @@ const CommentThread = ({ comment, depth = 0, onReply }) => {
|
||||||
>
|
>
|
||||||
<div className="bg-[#25272950] rounded-lg shadow-sm px-4 py-3 mb-2">
|
<div className="bg-[#25272950] rounded-lg shadow-sm px-4 py-3 mb-2">
|
||||||
<div className="flex gap-3 items-center justify-center">
|
<div className="flex gap-3 items-center justify-center">
|
||||||
{/* User Avatar SVG */}
|
<div className="flex-shrink-0">
|
||||||
<div className="flex-shrink-0 ">
|
|
||||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" className="rounded-full bg-[#6d9e37] p-1"><path d="M20 20C24.1421 20 27.5 16.6421 27.5 12.5C27.5 8.35786 24.1421 5 20 5C15.8579 5 12.5 8.35786 12.5 12.5C12.5 16.6421 15.8579 20 20 20ZM20 23.75C14.2675 23.75 5 26.4825 5 32.1875V35H35V32.1875C35 26.4825 25.7325 23.75 20 23.75Z" fill="white"/></svg>
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" className="rounded-full bg-[#6d9e37] p-1"><path d="M20 20C24.1421 20 27.5 16.6421 27.5 12.5C27.5 8.35786 24.1421 5 20 5C15.8579 5 12.5 8.35786 12.5 12.5C12.5 16.6421 15.8579 20 20 20ZM20 23.75C14.2675 23.75 5 26.4825 5 32.1875V35H35V32.1875C35 26.4825 25.7325 23.75 20 23.75Z" fill="white"/></svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex justify-between items-start mb-1">
|
<div className="flex justify-between items-start mb-1">
|
||||||
<span className="font-medium text-[#6d9e37]">{comment.user_name}</span>
|
<span className="font-medium text-[#6d9e37]">{comment.user_name}</span>
|
||||||
|
@ -38,27 +77,81 @@ const CommentThread = ({ comment, depth = 0, onReply }) => {
|
||||||
<p className="text-gray-100 mb-2 whitespace-pre-wrap">{comment.comment_text}</p>
|
<p className="text-gray-100 mb-2 whitespace-pre-wrap">{comment.comment_text}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{
|
|
||||||
isLoginCoockie === true && (
|
{isLoginCoockie === true && (
|
||||||
<Button variant="outline" className="border-none text-[#6d9e37] hover:text-[#8bc34a] hover:bg-transparent px-0 py-0 h-auto" onClick={() => setIsReplying(!isReplying)}>Reply</Button>
|
<Button
|
||||||
)
|
variant="outline"
|
||||||
}
|
className="border-none text-[#6d9e37] hover:text-[#8bc34a] hover:bg-transparent px-0 py-0 h-auto"
|
||||||
|
onClick={() => setIsReplying(!isReplying)}
|
||||||
|
>
|
||||||
|
Reply
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{isReplying && (
|
{isReplying && (
|
||||||
<form onSubmit={handleReplySubmit} className="mt-3 pl-11">
|
<form onSubmit={handleReplySubmit} className="mt-3 pl-11 relative">
|
||||||
<Textarea value={replyText} onChange={(e) => setReplyText(e.target.value)} placeholder="Write your reply..." className="mb-2 min-h-[70px]" required minLength={1} />
|
<Textarea
|
||||||
<div className="flex gap-2">
|
ref={textareaRef}
|
||||||
<Button type="submit" size="sm" className="bg-[#6d9e37] hover:bg-[#8bc34a]">Post Reply</Button>
|
value={replyText}
|
||||||
<Button type="button" variant="outline" size="sm" className="border-gray-600 text-gray-300 hover:border-[#6d9e37] hover:text-[#8bc34a]" onClick={() => setIsReplying(false)}>Cancel</Button>
|
onChange={(e) => setReplyText(e.target.value)}
|
||||||
|
placeholder="Write your reply..."
|
||||||
|
className="mb-2 min-h-[70px] pr-10"
|
||||||
|
required
|
||||||
|
minLength={1}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Emoji Button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id={`reply-emoji-btn-${comment.comment_id}`}
|
||||||
|
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
||||||
|
className="absolute bottom-4 right-4 text-xl text-gray-400 hover:text-yellow-500 transition"
|
||||||
|
title="Add Emoji"
|
||||||
|
>
|
||||||
|
<svg width="20px" height="20px" 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="M8.5 11C9.32843 11 10 10.3284 10 9.5C10 8.67157 9.32843 8 8.5 8C7.67157 8 7 8.67157 7 9.5C7 10.3284 7.67157 11 8.5 11Z" fill="#6d9e37"></path> <path d="M17 9.5C17 10.3284 16.3284 11 15.5 11C14.6716 11 14 10.3284 14 9.5C14 8.67157 14.6716 8 15.5 8C16.3284 8 17 8.67157 17 9.5Z" fill="#6d9e37"></path> <path d="M8.88875 13.5414C8.63822 13.0559 8.0431 12.8607 7.55301 13.1058C7.05903 13.3528 6.8588 13.9535 7.10579 14.4474C7.18825 14.6118 7.29326 14.7659 7.40334 14.9127C7.58615 15.1565 7.8621 15.4704 8.25052 15.7811C9.04005 16.4127 10.2573 17.0002 12.0002 17.0002C13.7431 17.0002 14.9604 16.4127 15.7499 15.7811C16.1383 15.4704 16.4143 15.1565 16.5971 14.9127C16.7076 14.7654 16.8081 14.6113 16.8941 14.4485C17.1387 13.961 16.9352 13.3497 16.4474 13.1058C15.9573 12.8607 15.3622 13.0559 15.1117 13.5414C15.0979 13.5663 14.9097 13.892 14.5005 14.2194C14.0401 14.5877 13.2573 15.0002 12.0002 15.0002C10.7431 15.0002 9.96038 14.5877 9.49991 14.2194C9.09071 13.892 8.90255 13.5663 8.88875 13.5414Z" fill="#6d9e37"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23ZM12 20.9932C7.03321 20.9932 3.00683 16.9668 3.00683 12C3.00683 7.03321 7.03321 3.00683 12 3.00683C16.9668 3.00683 20.9932 7.03321 20.9932 12C20.9932 16.9668 16.9668 20.9932 12 20.9932Z" fill="#6d9e37"></path> </g></svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Emoji Picker */}
|
||||||
|
{showEmojiPicker && (
|
||||||
|
<div
|
||||||
|
ref={emojiPickerRef}
|
||||||
|
className="absolute bottom-20 right-0 z-50 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<EmojiPicker
|
||||||
|
onEmojiClick={(emojiData) => insertEmojiAtCursor(emojiData.emoji)}
|
||||||
|
height={400}
|
||||||
|
width={300}
|
||||||
|
theme="dark"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2 mt-2">
|
||||||
|
<Button type="submit" size="sm" className="bg-[#6d9e37] hover:bg-[#8bc34a]">
|
||||||
|
Post Reply
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="border-gray-600 text-gray-300 hover:border-[#6d9e37] hover:text-[#8bc34a]"
|
||||||
|
onClick={() => {
|
||||||
|
setIsReplying(false);
|
||||||
|
setShowEmojiPicker(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{comment.replies?.map((reply) => (
|
{comment.replies?.map((reply) => (
|
||||||
<CommentThread key={reply.comment_id} comment={reply} depth={depth + 1} onReply={onReply}/>
|
<CommentThread key={reply.comment_id} comment={reply} depth={depth + 1} onReply={onReply} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CommentThread;
|
export default CommentThread;
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
.slider-container {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider img {
|
||||||
|
width: 100%;
|
||||||
|
height: 600px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-arrow, .right-arrow {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
font-size: 3rem;
|
||||||
|
color: #fff;
|
||||||
|
z-index: 10;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
border: none;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-arrow {
|
||||||
|
left: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-arrow {
|
||||||
|
right: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dots-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
margin: 0 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot.active {
|
||||||
|
color: #333;
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
|
||||||
|
function ImageSlider({ images }) {
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [startX, setStartX] = useState(0);
|
||||||
|
const [autoplay, setAutoplay] = useState(true);
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
|
const trackRef = useRef(null);
|
||||||
|
const autoplayRef = useRef(null);
|
||||||
|
// Autoplay
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoplay) {
|
||||||
|
autoplayRef.current = setInterval(() => {
|
||||||
|
setCurrentIndex(prev => (prev + 1) % images.length);
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
return () => clearInterval(autoplayRef.current);
|
||||||
|
}, [autoplay, images.length]);
|
||||||
|
|
||||||
|
// ESC key for fullscreen exit
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEsc = (e) => {
|
||||||
|
if (e.key === 'Escape') setIsFullscreen(false);
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleEsc);
|
||||||
|
return () => window.removeEventListener('keydown', handleEsc);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Drag Start
|
||||||
|
const handleDragStart = (e) => {
|
||||||
|
setIsDragging(true);
|
||||||
|
setAutoplay(false);
|
||||||
|
setStartX(e.type.includes('mouse') ? e.clientX : e.touches[0].clientX);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drag Move
|
||||||
|
const handleDragMove = (e) => {
|
||||||
|
if (!isDragging || !trackRef.current) return;
|
||||||
|
const currentX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
|
||||||
|
const diff = currentX - startX;
|
||||||
|
trackRef.current.style.transform = `translateX(calc(${-currentIndex * 100}% + ${diff}px))`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drag End
|
||||||
|
const handleDragEnd = (e) => {
|
||||||
|
if (!isDragging || !trackRef.current) return;
|
||||||
|
setIsDragging(false);
|
||||||
|
const endX = e.type.includes('mouse') ? e.clientX : e.changedTouches[0].clientX;
|
||||||
|
const diff = endX - startX;
|
||||||
|
const threshold = 50;
|
||||||
|
|
||||||
|
if (diff > threshold) {
|
||||||
|
setCurrentIndex(prev => (prev > 0 ? prev - 1 : images.length - 1));
|
||||||
|
} else if (diff < -threshold) {
|
||||||
|
setCurrentIndex(prev => (prev + 1) % images.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAutoplay(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToImage = (index) => {
|
||||||
|
setCurrentIndex(index);
|
||||||
|
setAutoplay(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToPrevious = () => {
|
||||||
|
setCurrentIndex(prev => (prev === 0 ? images.length - 1 : prev - 1));
|
||||||
|
setAutoplay(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToNext = () => {
|
||||||
|
setCurrentIndex(prev => (prev + 1) % images.length);
|
||||||
|
setAutoplay(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full max-w-5xl mx-auto select-none group">
|
||||||
|
{/* Main Slider */}
|
||||||
|
<div
|
||||||
|
className="relative overflow-hidden h-auto rounded-2xl shadow-2xl"
|
||||||
|
onMouseDown={handleDragStart}
|
||||||
|
onMouseMove={handleDragMove}
|
||||||
|
onMouseUp={handleDragEnd}
|
||||||
|
onMouseLeave={handleDragEnd}
|
||||||
|
onTouchStart={handleDragStart}
|
||||||
|
onTouchMove={handleDragMove}
|
||||||
|
onTouchEnd={handleDragEnd}
|
||||||
|
style={{ cursor: isDragging ? 'grabbing' : 'grab' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={trackRef}
|
||||||
|
className="flex transition-transform duration-500 ease-in-out"
|
||||||
|
style={{ transform: `translateX(-${currentIndex * 100}%)` }}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
images.length > 0 && (
|
||||||
|
images.map((img, idx) => (
|
||||||
|
<div key={idx} className="w-full flex-shrink-0">
|
||||||
|
<img src={`https://gallery-image-pocndhvgmcnbacgb.siliconpin.com${img.image}`} alt={`Slide ${idx}`} className="relative w-full h-full" draggable={false} />
|
||||||
|
<p className='absolute bottom-0 bg-black/70 lg:text-center text-[#FFF] z-40 w-full px-2 py-1 lg:py-2'>{img.desc}</p>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fullscreen Button */}
|
||||||
|
<div className="absolute bottom-4 left-4 z-30">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsFullscreen(true)}
|
||||||
|
className="bg-black/50 hover:bg-black/70 text-white p-2 rounded-full text-lg shadow-lg transition-all border-2 border-[#fff}"
|
||||||
|
title="Fullscreen"
|
||||||
|
>
|
||||||
|
<svg width="18px" height="18px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#ffffff"><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.7092 2.29502C21.8041 2.3904 21.8757 2.50014 21.9241 2.61722C21.9727 2.73425 21.9996 2.8625 22 2.997L22 3V9C22 9.55228 21.5523 10 21 10C20.4477 10 20 9.55228 20 9V5.41421L14.7071 10.7071C14.3166 11.0976 13.6834 11.0976 13.2929 10.7071C12.9024 10.3166 12.9024 9.68342 13.2929 9.29289L18.5858 4H15C14.4477 4 14 3.55228 14 3C14 2.44772 14.4477 2 15 2H20.9998C21.2749 2 21.5242 2.11106 21.705 2.29078L21.7092 2.29502Z" fill="#ffffff"></path> <path d="M10.7071 14.7071L5.41421 20H9C9.55228 20 10 20.4477 10 21C10 21.5523 9.55228 22 9 22H3.00069L2.997 22C2.74301 21.9992 2.48924 21.9023 2.29502 21.7092L2.29078 21.705C2.19595 21.6096 2.12432 21.4999 2.07588 21.3828C2.02699 21.2649 2 21.1356 2 21V15C2 14.4477 2.44772 14 3 14C3.55228 14 4 14.4477 4 15V18.5858L9.29289 13.2929C9.68342 12.9024 10.3166 12.9024 10.7071 13.2929C11.0976 13.6834 11.0976 14.3166 10.7071 14.7071Z" fill="#ffffff"></path> </g></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Arrows */}
|
||||||
|
<button
|
||||||
|
onClick={goToPrevious}
|
||||||
|
className="absolute top-1/2 left-3 -translate-y-1/2 z-20 bg-black/40 hover:bg-black/70 text-white rounded-full flex items-center justify-center md:opacity-0 group-hover:opacity-100 transition"
|
||||||
|
>
|
||||||
|
<svg width="32px" height="32px" 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="M11.7071 16.7071C12.0976 16.3166 12.0976 15.6834 11.7071 15.2929L9.41421 13H17C17.5523 13 18 12.5523 18 12C18 11.4477 17.5523 11 17 11H9.41421L11.7071 8.70711C12.0976 8.31658 12.0976 7.68342 11.7071 7.29289C11.3166 6.90237 10.6834 6.90237 10.2929 7.29289L6.29289 11.2929C5.90237 11.6834 5.90237 12.3166 6.29289 12.7071L10.2929 16.7071C10.6834 17.0976 11.3166 17.0976 11.7071 16.7071Z" fill="#ffffff"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23ZM21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" fill="#ffffff"></path> </g></svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={goToNext}
|
||||||
|
className="absolute top-1/2 right-3 -translate-y-1/2 z-20 bg-black/40 hover:bg-black/70 text-white rounded-full flex items-center justify-center md:opacity-0 group-hover:opacity-100 transition"
|
||||||
|
>
|
||||||
|
<svg width="32px" height="32px" 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="M12.2929 16.7071C11.9024 16.3166 11.9024 15.6834 12.2929 15.2929L14.5858 13H7C6.44772 13 6 12.5523 6 12C6 11.4477 6.44772 11 7 11H14.5858L12.2929 8.70711C11.9024 8.31658 11.9024 7.68342 12.2929 7.29289C12.6834 6.90237 13.3166 6.90237 13.7071 7.29289L17.7071 11.2929C18.0976 11.6834 18.0976 12.3166 17.7071 12.7071L13.7071 16.7071C13.3166 17.0976 12.6834 17.0976 12.2929 16.7071Z" fill="#ffffff"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 23C5.92487 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23ZM3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12Z" fill="#ffffff"></path> </g></svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Thumbnail Navigation */}
|
||||||
|
<div className="mt-4 flex overflow-x-auto gap-2">
|
||||||
|
{
|
||||||
|
images.length > 0 && (
|
||||||
|
images.map((img, idx) => (
|
||||||
|
<img
|
||||||
|
key={idx}
|
||||||
|
src={`https://gallery-image-pocndhvgmcnbacgb.siliconpin.com${img.image}`}
|
||||||
|
alt={`Thumb ${idx}`}
|
||||||
|
onClick={() => goToImage(idx)}
|
||||||
|
className={`w-20 h-14 object-cover rounded-md cursor-pointer border-2 transition ${
|
||||||
|
idx === currentIndex ? 'border-white' : 'border-transparent'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fullscreen Overlay */}
|
||||||
|
{images.length > 0 && isFullscreen && (
|
||||||
|
<div className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center">
|
||||||
|
<img src={`https://gallery-image-pocndhvgmcnbacgb.siliconpin.com${images[currentIndex].image}`} alt="Fullscreen" className="relative max-w-full max-h-[80vh] object-contain transition duration-300" />
|
||||||
|
<p className='absolute bottom-0 bg-black/70 lg:text-center text-[#FFF] z-100 w-full px-2 py-1 lg:py-2'>{images[currentIndex].desc}</p>
|
||||||
|
<button onClick={() => setIsFullscreen(false)} className="absolute top-4 left-4 text-white text-2xl bg-white/20 hover:bg-white/40 rounded-full" title="Close" >
|
||||||
|
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title>cross-circle</title> <desc>Created with Sketch Beta.</desc> <defs> </defs> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"> <g id="Icon-Set" sketch:type="MSLayerGroup" transform="translate(-568.000000, -1087.000000)" fill="#ffffff"> <path d="M584,1117 C576.268,1117 570,1110.73 570,1103 C570,1095.27 576.268,1089 584,1089 C591.732,1089 598,1095.27 598,1103 C598,1110.73 591.732,1117 584,1117 L584,1117 Z M584,1087 C575.163,1087 568,1094.16 568,1103 C568,1111.84 575.163,1119 584,1119 C592.837,1119 600,1111.84 600,1103 C600,1094.16 592.837,1087 584,1087 L584,1087 Z M589.717,1097.28 C589.323,1096.89 588.686,1096.89 588.292,1097.28 L583.994,1101.58 L579.758,1097.34 C579.367,1096.95 578.733,1096.95 578.344,1097.34 C577.953,1097.73 577.953,1098.37 578.344,1098.76 L582.58,1102.99 L578.314,1107.26 C577.921,1107.65 577.921,1108.29 578.314,1108.69 C578.708,1109.08 579.346,1109.08 579.74,1108.69 L584.006,1104.42 L588.242,1108.66 C588.633,1109.05 589.267,1109.05 589.657,1108.66 C590.048,1108.27 590.048,1107.63 589.657,1107.24 L585.42,1103.01 L589.717,1098.71 C590.11,1098.31 590.11,1097.68 589.717,1097.28 L589.717,1097.28 Z" id="cross-circle" sketch:type="MSShapeGroup"> </path> </g> </g> </g></svg>
|
||||||
|
</button>
|
||||||
|
<button onClick={goToPrevious} className="absolute left-4 top-1/2 -translate-y-1/2 text-white text-3xl bg-white/10 hover:bg-white/30 rounded-full">
|
||||||
|
<svg width="32px" height="32px" 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="M11.7071 16.7071C12.0976 16.3166 12.0976 15.6834 11.7071 15.2929L9.41421 13H17C17.5523 13 18 12.5523 18 12C18 11.4477 17.5523 11 17 11H9.41421L11.7071 8.70711C12.0976 8.31658 12.0976 7.68342 11.7071 7.29289C11.3166 6.90237 10.6834 6.90237 10.2929 7.29289L6.29289 11.2929C5.90237 11.6834 5.90237 12.3166 6.29289 12.7071L10.2929 16.7071C10.6834 17.0976 11.3166 17.0976 11.7071 16.7071Z" fill="#ffffff"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23ZM21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" fill="#ffffff"></path> </g></svg>
|
||||||
|
</button>
|
||||||
|
<button onClick={goToNext} className="absolute right-4 top-1/2 -translate-y-1/2 text-white text-3xl bg-white/10 hover:bg-white/30 rounded-full">
|
||||||
|
<svg width="32px" height="32px" 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="M12.2929 16.7071C11.9024 16.3166 11.9024 15.6834 12.2929 15.2929L14.5858 13H7C6.44772 13 6 12.5523 6 12C6 11.4477 6.44772 11 7 11H14.5858L12.2929 8.70711C11.9024 8.31658 11.9024 7.68342 12.2929 7.29289C12.6834 6.90237 13.3166 6.90237 13.7071 7.29289L17.7071 11.2929C18.0976 11.6834 18.0976 12.3166 17.7071 12.7071L13.7071 16.7071C13.3166 17.0976 12.6834 17.0976 12.2929 16.7071Z" fill="#ffffff"></path> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 23C5.92487 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23ZM3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12Z" fill="#ffffff"></path> </g></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageSlider;
|
|
@ -353,6 +353,37 @@ const NewTopic = () => {
|
||||||
<Input type="text" name="title" value={formData.title} onChange={handleChange} placeholder="Enter a descriptive title" required />
|
<Input type="text" name="title" value={formData.title} onChange={handleChange} placeholder="Enter a descriptive title" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Featured Image Upload */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="image">Featured Image <span className="text-xs text-[#4B5563]">(Hero image)</span></Label>
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
id="image"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept="image/*"
|
||||||
|
className="cursor-pointer"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{uploadProgress > 0 && uploadProgress < 100 && (
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||||||
|
<div
|
||||||
|
className="bg-blue-600 h-2.5 rounded-full"
|
||||||
|
style={{ width: `${uploadProgress}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formData.imageUrl && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<img
|
||||||
|
src={formData.imageUrl}
|
||||||
|
alt="Preview"
|
||||||
|
className="max-h-40 rounded-md border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* <div className="space-y-2">
|
{/* <div className="space-y-2">
|
||||||
<Label htmlFor="slug">URL Slug</Label>
|
<Label htmlFor="slug">URL Slug</Label>
|
||||||
<Input type="text" name="slug" value={formData.slug} onChange={handleChange} readOnly className="bg-gray-50" />
|
<Input type="text" name="slug" value={formData.slug} onChange={handleChange} readOnly className="bg-gray-50" />
|
||||||
|
@ -459,37 +490,6 @@ const NewTopic = () => {
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Featured Image Upload */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="image">Featured Image</Label>
|
|
||||||
<Input
|
|
||||||
type="file"
|
|
||||||
id="image"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
accept="image/*"
|
|
||||||
className="cursor-pointer"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{uploadProgress > 0 && uploadProgress < 100 && (
|
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
|
||||||
<div
|
|
||||||
className="bg-blue-600 h-2.5 rounded-full"
|
|
||||||
style={{ width: `${uploadProgress}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{formData.imageUrl && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<img
|
|
||||||
src={formData.imageUrl}
|
|
||||||
alt="Preview"
|
|
||||||
className="max-h-40 rounded-md border"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
{/* Form Actions */}
|
{/* Form Actions */}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
import ImageSlider from "./ImageSlider/ImageSlider"
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
|
||||||
import { Label } from "./ui/label";
|
import { Label } from "./ui/label";
|
||||||
|
@ -7,15 +8,29 @@ import { Button } from "./ui/button";
|
||||||
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||||
import { token, user_name, pb_id } from '../lib/CookieValues';
|
import { token, user_name, pb_id } from '../lib/CookieValues';
|
||||||
import CommentSystem from './CommentSystem/CommentSystem';
|
import CommentSystem from './CommentSystem/CommentSystem';
|
||||||
|
|
||||||
const COMMENTS_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/comments/';
|
const COMMENTS_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/comments/';
|
||||||
export default function TopicDetail(props) {
|
export default function TopicDetail(props) {
|
||||||
// console.log('coockie data', user_name)
|
// console.log('coockie data', user_name)
|
||||||
const [showCopied, setShowCopied] = useState(false);
|
const [showCopied, setShowCopied] = useState(false);
|
||||||
|
const [allGalleryImages, setAllGalleryImages] = useState([]);
|
||||||
if (!props.topic) {
|
if (!props.topic) {
|
||||||
return <div>Topic not found</div>;
|
return <div>Topic not found</div>;
|
||||||
}
|
}
|
||||||
|
const slug = props.topic.slug;
|
||||||
|
const fetchGalleryImage = async () => {
|
||||||
|
try{
|
||||||
|
const response = await fetch(`https://gallery-image-pocndhvgmcnbacgb.siliconpin.com/gallery-image-delevery?slug=${slug}`)
|
||||||
|
const data = await response.json();
|
||||||
|
setAllGalleryImages(data);
|
||||||
|
// console.log('data.images', data.images)
|
||||||
|
}catch(error){
|
||||||
|
// console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
fetchGalleryImage()
|
||||||
|
}, [slug])
|
||||||
const shareUrl = typeof window !== 'undefined' ? window.location.href : '';
|
const shareUrl = typeof window !== 'undefined' ? window.location.href : '';
|
||||||
const title = props.topic.title;
|
const title = props.topic.title;
|
||||||
const text = `Check out this Topic: ${title}`;
|
const text = `Check out this Topic: ${title}`;
|
||||||
|
@ -195,6 +210,11 @@ export default function TopicDetail(props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="markdown-content" className="table-scroll-wrapper font-light mb-8 text-justify prose max-w-none" dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }} ></div>
|
<div id="markdown-content" className="table-scroll-wrapper font-light mb-8 text-justify prose max-w-none" dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }} ></div>
|
||||||
|
{
|
||||||
|
allGalleryImages && (
|
||||||
|
<ImageSlider images={allGalleryImages} />
|
||||||
|
)
|
||||||
|
}
|
||||||
<CommentSystem topicId={props.topic.id}/>
|
<CommentSystem topicId={props.topic.id}/>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -32,6 +32,22 @@ export default function EditTopic (){
|
||||||
const [imageUploadFile, setImageUploadFile] = useState(null);
|
const [imageUploadFile, setImageUploadFile] = useState(null);
|
||||||
const [imageUploadPreview, setImageUploadPreview] = useState('');
|
const [imageUploadPreview, setImageUploadPreview] = useState('');
|
||||||
const [editorMode, setEditorMode] = useState('edit');
|
const [editorMode, setEditorMode] = useState('edit');
|
||||||
|
const [galleryImgDesc, setGalleryImgDesc] = useState('');
|
||||||
|
const [galleryImg, setGalleryImg] = useState('');
|
||||||
|
const [statusMessage, setStatusMessage] = useState({success : "", message: ""});
|
||||||
|
const [allGalleryImages, setAllGalleryImages] = useState([]);
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
|
const fetchGalleryImage = async () => {
|
||||||
|
try{
|
||||||
|
const response = await fetch(`https://gallery-image-pocndhvgmcnbacgb.siliconpin.com/gallery-image-delevery?slug=${slug}`)
|
||||||
|
const data = await response.json();
|
||||||
|
setAllGalleryImages(data.images);
|
||||||
|
// console.log('data.images', data.images)
|
||||||
|
}catch(error){
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch topic data on component mount
|
// Fetch topic data on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -54,7 +70,7 @@ export default function EditTopic (){
|
||||||
title: topic.title || '',
|
title: topic.title || '',
|
||||||
slug: topic.slug || '',
|
slug: topic.slug || '',
|
||||||
content: topic.content || '',
|
content: topic.content || '',
|
||||||
imageUrl: topic.imageUrl || ''
|
imageUrl: topic.img || ''
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
|
@ -63,8 +79,39 @@ export default function EditTopic (){
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchTopic();
|
fetchTopic(); fetchGalleryImage();
|
||||||
}, [slug]);
|
}, [slug]);
|
||||||
|
// Upload Gallery Images
|
||||||
|
const uploadGalleryImages = async (file, slug) => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('description', galleryImgDesc);
|
||||||
|
formData.append('slug', slug);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://gallery-image-pocndhvgmcnbacgb.siliconpin.com/upload`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success === true) {
|
||||||
|
setGalleryImgDesc('');
|
||||||
|
setStatusMessage({ success: true, message: "Image Uploaded! you can upload more images" });
|
||||||
|
setGalleryImg(null);
|
||||||
|
fetchGalleryImage();
|
||||||
|
if (fileInputRef.current) fileInputRef.current.value = null;
|
||||||
|
} else {
|
||||||
|
setStatusMessage({ success: false, message: "Failed to upload image" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setStatusMessage({ success: false, message: "Failed to upload image" });
|
||||||
|
}
|
||||||
|
setIsSubmitting(false);
|
||||||
|
};
|
||||||
|
|
||||||
// Upload file to MinIO (same as NewTopic)
|
// Upload file to MinIO (same as NewTopic)
|
||||||
const uploadToMinIO = async (file, onProgress) => {
|
const uploadToMinIO = async (file, onProgress) => {
|
||||||
|
@ -399,6 +446,43 @@ export default function EditTopic (){
|
||||||
<p className="text-xs text-gray-500">This will be used in the topic URL</p>
|
<p className="text-xs text-gray-500">This will be used in the topic URL</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Featured Image Upload */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label >Featured Image</Label>
|
||||||
|
<div className="flex flex-wrap space-x-4">
|
||||||
|
<Label htmlFor="image" className="cursor-pointer">
|
||||||
|
<svg viewBox="0 0 512 512" width="100px" height="100px" fill="#000000">
|
||||||
|
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
||||||
|
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
|
||||||
|
<g id="SVGRepo_iconCarrier">
|
||||||
|
<path fill="#6d9e37" d="M512,0h-40v16h24v32h16V0z M432,0h-40v16h40V0z M352,0h-40v16h40V0z M272,0h-40v16h40V0z M192,0h-40 v16h40V0z M112,0H72v16h40V0z M32,0H0v16h32V0z M16,48H0v40h16V48z M16,128H0v40h16V128z M16,208H0v40h16V208z M16,288H0v40h16V288z M16,368H0v40h16V368z M16,448H0v40h16V448z M56,496H16v16h40V496z M136,496H96v16h40V496z M216,496h-40v16h40V496z M296,496h-40v16 h40V496z M376,496h-40v16h40V496z M456,496h-40v16h40V496z M512,488h-16v8l0,0v16h16V488z M512,408h-16v40h16V408z M512,328h-16v40 h16V328z M512,248h-16v40h16V248z M512,168h-16v40h16V168z M512,88h-16v40h16V88z"></path>
|
||||||
|
<g>
|
||||||
|
<rect x="244" y="175.976" fill="#6d9e37" width="24" height="160.08"></rect>
|
||||||
|
<rect x="175.976" y="244" fill="#6d9e37" width="160.08" height="24"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
id="image"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept="image/*"
|
||||||
|
className="cursor-pointer hidden"
|
||||||
|
/>
|
||||||
|
{formData.imageUrl && (
|
||||||
|
<div className="relative group">
|
||||||
|
<img
|
||||||
|
src={formData.imageUrl}
|
||||||
|
alt="Preview"
|
||||||
|
className="w-[100px] h-[100px] rounded-md border"
|
||||||
|
/>
|
||||||
|
<button onClick={(e) => {e.preventDefault(); setFormData({...formData, imageUrl : null})}} className="absolute inset-0 opacity-0 group-hover:opacity-100 bg-white/20 transition-all duration-500 text-2xl font-bold text-red-500">X</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Content Editor */}
|
{/* Content Editor */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
|
@ -496,37 +580,42 @@ export default function EditTopic (){
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
{
|
||||||
{/* Featured Image Upload */}
|
allGalleryImages && (
|
||||||
<div className="space-y-2">
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 gap-4 mt-4">
|
||||||
<Label htmlFor="image">Featured Image</Label>
|
{allGalleryImages.map((img, index) => (
|
||||||
<Input
|
<img key={index} src={`https://gallery-image-pocndhvgmcnbacgb.siliconpin.com${img}`} alt={`gallery-${index}`} className="w-full h-auto rounded shadow" />
|
||||||
type="file"
|
))}
|
||||||
id="image"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
accept="image/*"
|
|
||||||
className="cursor-pointer"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{uploadProgress > 0 && uploadProgress < 100 && (
|
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
|
||||||
<div
|
|
||||||
className="bg-blue-600 h-2.5 rounded-full"
|
|
||||||
style={{ width: `${uploadProgress}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
{formData.imageUrl && (
|
<div>
|
||||||
<div className="mt-2">
|
<Label htmlFor="galleryImage">
|
||||||
<img
|
Gallery Image: <span className="text-xs text-gray-500">(First write description (Min 10 Char) to proceed image upload!)</span>
|
||||||
src={formData.imageUrl}
|
</Label>
|
||||||
alt="Preview"
|
<div className="flex gap-x-4 mt-1">
|
||||||
className="max-h-40 rounded-md border"
|
<Input
|
||||||
/>
|
onChange={(e) => setGalleryImgDesc(e.target.value)}
|
||||||
<p className="text-xs text-gray-500 mt-1">Current Image</p>
|
value={galleryImgDesc}
|
||||||
</div>
|
type="text"
|
||||||
)}
|
placeholder="Write description to proceed image upload!"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={(e) => setGalleryImg(e.target.files[0])}
|
||||||
|
type="file"
|
||||||
|
className={`${galleryImgDesc.trim().length > 9 ? 'border-[#6d9e37] file:bg-[#6d9e37] file:text-white file:border-0 file:rounded-md' : ''}`}
|
||||||
|
disabled={galleryImgDesc.trim().length < 10}
|
||||||
|
/>
|
||||||
|
<Button onClick={() => uploadGalleryImages(galleryImg, slug)} disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? 'Uploading...' : 'Upload'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className={`mt-2 text-center ${statusMessage.success === true ? 'text-[#6d9e37]' : statusMessage.success === false ? 'text-red-500' : ''}`}>
|
||||||
|
{statusMessage.success === true && <span className="mr-1">✓</span>}
|
||||||
|
{statusMessage.success === false && <span className="mr-1">❌</span>}
|
||||||
|
{statusMessage.message}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
|
@ -54,7 +54,7 @@ export default function TopicItems(props) {
|
||||||
<CardTitle className="mb-2 line-clamp-1">{topic.title}</CardTitle>
|
<CardTitle className="mb-2 line-clamp-1">{topic.title}</CardTitle>
|
||||||
<CardDescription className="line-clamp-4 mb-4 text-justify" dangerouslySetInnerHTML={{ __html: marked.parse(topic.content || '') }}></CardDescription>
|
<CardDescription className="line-clamp-4 mb-4 text-justify" dangerouslySetInnerHTML={{ __html: marked.parse(topic.content || '') }}></CardDescription>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<p className="text-xs text-gray-500"><strong>Author: </strong>{topic.user.split('@')[0]}</p>
|
<p className="text-xs text-gray-500"><strong>Author: </strong>{topic.user_name}</p>
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
<strong>Date: </strong>
|
<strong>Date: </strong>
|
||||||
{new Date(topic.timestamp).toLocaleDateString('en-GB', {
|
{new Date(topic.timestamp).toLocaleDateString('en-GB', {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import LoginProfile from '../components/LoginOrProfile';
|
||||||
|
|
||||||
interface Props { title: string; description?: string; ogImage?: string; canonicalURL?: string; type?: 'website' | 'article'; keyWords?: string; newSchema1 : string; newSchema2 : string; newSchema3 : string; newSchema4 : string; newSchema5 : string}
|
interface Props { title: string; description?: string; ogImage?: string; canonicalURL?: string; type?: 'website' | 'article'; keyWords?: string; newSchema1 : string; newSchema2 : string; newSchema3 : string; newSchema4 : string; newSchema5 : string}
|
||||||
|
|
||||||
const { title, description, ogImage, canonicalURL = new URL(Astro.url.pathname, Astro.site).href, type = "website", keyWords, newSchema1, newSchema2, newSchema3, newSchema4, newSchema5} = Astro.props;
|
const { title, description, ogImage, canonicalURL, type = "website", keyWords, newSchema1, newSchema2, newSchema3, newSchema4, newSchema5} = Astro.props;
|
||||||
// Organization schema
|
// Organization schema
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -371,32 +371,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
#markdown-content > table, th, tbody, td{
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
#markdown-content > table > thead > tr > th{
|
|
||||||
text-align: center;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#markdown-content > table > thead > tr > th:first-child,
|
|
||||||
#markdown-content > table > tbody > tr > td:first-child {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
#markdown-content > table > thead > tr > th:not(:first-child),
|
|
||||||
#markdown-content > table > tbody > tr > td:not(:first-child) {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-scroll-wrapper > table {
|
|
||||||
overflow-x: auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.table-scroll-wrapper > table {
|
|
||||||
min-width: 600px; /* adjust as needed */
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -12,6 +12,7 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
|
||||||
description={pageDescription}
|
description={pageDescription}
|
||||||
ogImage={pageImage}
|
ogImage={pageImage}
|
||||||
type="website"
|
type="website"
|
||||||
|
canonicalURL="/about-us"
|
||||||
>
|
>
|
||||||
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
||||||
<div class="text-center mb-8 sm:mb-12">
|
<div class="text-center mb-8 sm:mb-12">
|
||||||
|
|
|
@ -109,6 +109,7 @@ const contactSchema3 = {
|
||||||
description={metaDescription}
|
description={metaDescription}
|
||||||
ogImage={ogImage}
|
ogImage={ogImage}
|
||||||
type="website"
|
type="website"
|
||||||
|
canonicalURL="/contact-us"
|
||||||
newSchema1={JSON.stringify(contactSchema1)}
|
newSchema1={JSON.stringify(contactSchema1)}
|
||||||
newSchema2={JSON.stringify(contactSchema2)}
|
newSchema2={JSON.stringify(contactSchema2)}
|
||||||
newSchema3={JSON.stringify(contactSchema3)}
|
newSchema3={JSON.stringify(contactSchema3)}
|
||||||
|
|
|
@ -19,6 +19,7 @@ const defaultSubdomain = randomString();
|
||||||
description={pageDescription}
|
description={pageDescription}
|
||||||
ogImage={pageImage}
|
ogImage={pageImage}
|
||||||
type="website"
|
type="website"
|
||||||
|
canonicalURL="/get-started"
|
||||||
>
|
>
|
||||||
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
||||||
<div class="text-center mb-8 sm:mb-12">
|
<div class="text-center mb-8 sm:mb-12">
|
||||||
|
|
|
@ -107,6 +107,7 @@ const ogImage = "/assets/logo.svg";
|
||||||
newSchema3={JSON.stringify(schema3)}
|
newSchema3={JSON.stringify(schema3)}
|
||||||
newSchema4={'here is another schema content'}
|
newSchema4={'here is another schema content'}
|
||||||
newSchema5={'here is another schema content'}
|
newSchema5={'here is another schema content'}
|
||||||
|
canonicalURL="/"
|
||||||
|
|
||||||
>
|
>
|
||||||
<!-- Canvas background animation -->
|
<!-- Canvas background animation -->
|
||||||
|
|
|
@ -12,6 +12,7 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
|
||||||
description={pageDescription}
|
description={pageDescription}
|
||||||
ogImage={pageImage}
|
ogImage={pageImage}
|
||||||
type="website"
|
type="website"
|
||||||
|
canonicslURL="/privacy-policy"
|
||||||
>
|
>
|
||||||
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
||||||
<div class="text-center mb-8 sm:mb-12">
|
<div class="text-center mb-8 sm:mb-12">
|
||||||
|
|
|
@ -4,6 +4,6 @@ import Layout from "../../layouts/Layout.astro";
|
||||||
import UserProfile from "../../components/UserProfile";
|
import UserProfile from "../../components/UserProfile";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Profile Page">
|
<Layout title="Profile Page" canonicalURL="/profile">
|
||||||
<UserProfile client:load />
|
<UserProfile client:load />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
import Layout from "../../layouts/Layout.astro";
|
import Layout from "../../layouts/Layout.astro";
|
||||||
import BuyVPN from "../../components/BuyServices/BuyVPN"
|
import BuyVPN from "../../components/BuyServices/BuyVPN"
|
||||||
---
|
---
|
||||||
<Layout title="Buy VPN | WireGuard | SiliconPin">
|
<Layout title="Buy VPN | WireGuard | SiliconPin" canonicalURL="/services/buy-vpn">
|
||||||
<BuyVPN client:load />
|
<BuyVPN client:load />
|
||||||
</Layout>
|
</Layout>
|
|
@ -3,6 +3,6 @@ import Layout from "../../layouts/Layout.astro"
|
||||||
import NewHetznerCloudInstance from "../../components/BuyServices/NewHetznerCloudInstance";
|
import NewHetznerCloudInstance from "../../components/BuyServices/NewHetznerCloudInstance";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Cloud Instance | SiliconPin">
|
<Layout title="Cloud Instance | SiliconPin" canonicalURL="/services/cloud-hetzner-instance">
|
||||||
<NewHetznerCloudInstance client:load />
|
<NewHetznerCloudInstance client:load />
|
||||||
</Layout>
|
</Layout>
|
|
@ -3,6 +3,6 @@ import Layout from "../../layouts/Layout.astro"
|
||||||
import NewUthoCloudInstance from "../../components/BuyServices/NewUthoCloudInstance";
|
import NewUthoCloudInstance from "../../components/BuyServices/NewUthoCloudInstance";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Cloud Instance | SiliconPin">
|
<Layout title="Cloud Instance | SiliconPin" canonicalURL="/services/cloud-instance">
|
||||||
<NewUthoCloudInstance client:load />
|
<NewUthoCloudInstance client:load />
|
||||||
</Layout>
|
</Layout>
|
|
@ -2,6 +2,6 @@
|
||||||
import Layout from "../../layouts/Layout.astro"
|
import Layout from "../../layouts/Layout.astro"
|
||||||
import NewDroplet from "../../components/BuyServices/NewDroplet";
|
import NewDroplet from "../../components/BuyServices/NewDroplet";
|
||||||
---
|
---
|
||||||
<Layout title="Create Droplets | SiliconPin">
|
<Layout title="Create Droplets | SiliconPin" canonicalURL="/services/create-droplets">
|
||||||
<NewDroplet client:load />
|
<NewDroplet client:load />
|
||||||
</Layout>
|
</Layout>
|
|
@ -231,6 +231,7 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
|
||||||
ogImage={pageImage}
|
ogImage={pageImage}
|
||||||
type="website"
|
type="website"
|
||||||
newSchema1={JSON.stringify(schema1)}
|
newSchema1={JSON.stringify(schema1)}
|
||||||
|
canonicalURL="/services"
|
||||||
>
|
>
|
||||||
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
|
||||||
<div class="text-center mb-8 sm:mb-12">
|
<div class="text-center mb-8 sm:mb-12">
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
import Layout from "../../layouts/Layout.astro";
|
||||||
|
import NewKubernetisUtho from "../../components/BuyServices/NewKubernetisUtho";
|
||||||
|
---
|
||||||
|
<Layout title="Deploy Kubernetis | Kubernetis | SiliconPin" canonicalURL="/services/kubernetes">
|
||||||
|
<NewKubernetisUtho client:load />
|
||||||
|
</Layout>
|
|
@ -2,6 +2,6 @@
|
||||||
import Layout from "../../layouts/Layout.astro";
|
import Layout from "../../layouts/Layout.astro";
|
||||||
import NewKubernetisUtho from "../../components/BuyServices/NewKubernetisUtho";
|
import NewKubernetisUtho from "../../components/BuyServices/NewKubernetisUtho";
|
||||||
---
|
---
|
||||||
<Layout title="Deploy Kubernetis | Kubernetis | SiliconPin">
|
<Layout title="Deploy Kubernetis | Kubernetis | SiliconPin" canonicalURL="/services/kubernetes">
|
||||||
<NewKubernetisUtho client:load />
|
<NewKubernetisUtho client:load />
|
||||||
</Layout>
|
</Layout>
|
|
@ -2,6 +2,6 @@
|
||||||
import Layout from "../../layouts/Layout.astro"
|
import Layout from "../../layouts/Layout.astro"
|
||||||
import STTStreaming from "../../components/BuyServices/STTStreaming"
|
import STTStreaming from "../../components/BuyServices/STTStreaming"
|
||||||
---
|
---
|
||||||
<Layout title="STT Streaming | SiliconPin">
|
<Layout title="STT Streaming | SiliconPin" canonicalURL="/services/stt-streaming">
|
||||||
<STTStreaming client:load />
|
<STTStreaming client:load />
|
||||||
</Layout>
|
</Layout>
|
|
@ -48,11 +48,36 @@ export async function getStaticPaths() {
|
||||||
// console.log(topic)
|
// console.log(topic)
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={`${topic.title ? topic.title + '- Silicon Topic' : 'Silicon Topic'}`} ogImage={topic.img? topic.img : '/assets/images/thumb-place.jpg'} >
|
<Layout title={`${topic.title ? topic.title + '- Silicon Topic' : 'Silicon Topic'}`} ogImage={topic.img? topic.img : '/assets/images/thumb-place.jpg'} canonicalURL={`/topic/${topic.slug}`} >
|
||||||
<TopicDetail client:load topic={topic} />
|
<TopicDetail client:load topic={topic} />
|
||||||
</Layout>
|
</Layout>
|
||||||
<script>
|
<style>
|
||||||
</script>
|
#markdown-content > table, th, tbody, td{
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
#markdown-content > table > thead > tr > th{
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#markdown-content > table > thead > tr > th:first-child,
|
||||||
|
#markdown-content > table > tbody > tr > td:first-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
<!-- topic?.title ?? 'Silicon Topic | SiliconPin' -->
|
#markdown-content > table > thead > tr > th:not(:first-child),
|
||||||
|
#markdown-content > table > tbody > tr > td:not(:first-child) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-scroll-wrapper > table {
|
||||||
|
overflow-x: auto;
|
||||||
|
width: 100%;
|
||||||
|
table-layout: auto;
|
||||||
|
}
|
||||||
|
.table-scroll-wrapper > table {
|
||||||
|
min-width: 600px; /* adjust as needed */
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -23,6 +23,6 @@ try {
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Topics">
|
<Layout title="Topics" canonicalURL="/topic">
|
||||||
<TopicsList client:load />
|
<TopicsList client:load />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -3,6 +3,9 @@ export default {
|
||||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
boxShadow: {
|
||||||
|
xl: '0 10px 30px rgba(0, 0, 0, 0.3)',
|
||||||
|
},
|
||||||
colors: {
|
colors: {
|
||||||
'sp-dark': '#2b323b',
|
'sp-dark': '#2b323b',
|
||||||
'sp-light': '#c7ccd5',
|
'sp-light': '#c7ccd5',
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -2382,6 +2382,13 @@ electron-to-chromium@^1.5.73:
|
||||||
"@types/node" "^20.9.0"
|
"@types/node" "^20.9.0"
|
||||||
extract-zip "^2.0.1"
|
extract-zip "^2.0.1"
|
||||||
|
|
||||||
|
emoji-picker-react@^4.12.3:
|
||||||
|
version "4.12.3"
|
||||||
|
resolved "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.12.3.tgz"
|
||||||
|
integrity sha512-Pf+pTenW/uM+Juw197dbCtcB45uqvl9Y5/BzK44L72Aqvavt4UPmCqb+w0UKzUJkzh0Tp1rThfHVwoQXXbOZvQ==
|
||||||
|
dependencies:
|
||||||
|
flairup "1.0.0"
|
||||||
|
|
||||||
emoji-regex-xs@^1.0.0:
|
emoji-regex-xs@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz"
|
||||||
|
@ -2639,6 +2646,11 @@ filter-obj@^1.1.0:
|
||||||
resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz"
|
resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz"
|
||||||
integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==
|
integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==
|
||||||
|
|
||||||
|
flairup@1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz"
|
||||||
|
integrity sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==
|
||||||
|
|
||||||
flattie@^1.1.1:
|
flattie@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz"
|
resolved "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz"
|
||||||
|
|
Loading…
Reference in New Issue