add image gallery slider & emoji in topic singl page and othrs improvment

main
suvodip ghosh 2025-07-11 06:49:12 +00:00
parent 0634c26138
commit 9f2f235c09
29 changed files with 740 additions and 183 deletions

22
package-lock.json generated
View File

@ -26,6 +26,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"emoji-picker-react": "^4.12.3",
"framer-motion": "^12.18.1",
"image-resize-compress": "^2.1.1",
"js-cookie": "^3.0.5",
@ -6729,6 +6730,21 @@
"integrity": "sha512-oTUp3gfX1gZI+xfD2djr2rzQdHCwHzPQrrK0CD7WpTdF0nPdQ/INcRVjWgLdCT4a9W3jFObR9DAfsuyFQnI8CQ==",
"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": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
@ -7135,6 +7151,12 @@
"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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz",

View File

@ -29,6 +29,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"emoji-picker-react": "^4.12.3",
"framer-motion": "^12.18.1",
"image-resize-compress": "^2.1.1",
"js-cookie": "^3.0.5",

View File

@ -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 { Button } from '../ui/button';
import { Textarea } from '../ui/textarea';
import CommentThread from './CommentThread';
import { token, user_name, pb_id, siliconId, isLogin } from '../../lib/CookieValues';
import { useIsLoggedIn } from '../../lib/isLoggedIn';
const CommentSystem = ({ topicId, pbId }) => {
const isLoginCoockie = JSON.parse(isLogin);
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
@ -12,7 +14,46 @@ const CommentSystem = ({ topicId, pbId }) => {
const [newComment, setNewComment] = useState('');
const [commentsLoading, setCommentsLoading] = useState(false);
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 commentMap = {};
const rootComments = [];
@ -40,16 +81,12 @@ const CommentSystem = ({ topicId, pbId }) => {
setCommentsLoading(true);
setCommentsError('');
const params = new URLSearchParams({
topic_id: String(topicId),
});
const params = new URLSearchParams({ topic_id: String(topicId) });
// Only add silicon_id if it has a value
if (siliconId && siliconId !== 'null') {
params.append('silicon_id', siliconId);
}
// Only add pb_id if it has a value
if (pb_id && pb_id !== 'null') {
params.append('pb_id', pb_id);
}
@ -65,7 +102,6 @@ const CommentSystem = ({ topicId, pbId }) => {
setComments(nestComments(flatComments || []));
} catch (err) {
setCommentsError(err.message);
console.error('Fetch error:', err);
} finally {
setCommentsLoading(false);
}
@ -78,11 +114,11 @@ const CommentSystem = ({ topicId, pbId }) => {
const payload = {
topic_id: String(topicId),
silicon_id: siliconId,
pb_id: pb_id,
pb_id: pbId,
user_id: siliconId,
user_name: user_name,
comment_text: text,
is_approved: true
is_approved: true,
};
if (parentId) {
@ -94,7 +130,7 @@ const CommentSystem = ({ topicId, pbId }) => {
const response = await fetch(`https://sp-comments-prod-pocndhvgmcnbacgb.siliconpin.com${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
body: JSON.stringify(payload),
});
if (!response.ok) {
@ -139,19 +175,19 @@ const CommentSystem = ({ topicId, pbId }) => {
)}
</div>
)}
<div className='relative'>
{
isLoginCoockie !== true ? (
<div className="relative">
{isLoginCoockie !== true ? (
<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'>
<p className='italic bg-[#2a2e35] px-2 py-1.5 rounded-md shadow-xl'>To join the conversation, please log in first.</p>
<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>
</div>
</div>
) : (
''
)
}
<Card className="mt-8 p-6">
) : null}
<Card className="mt-8 p-6 shadow-md border border-gray-200">
<h3 className="text-lg font-medium mb-4">Add a Comment</h3>
<form onSubmit={(e) => {
e.preventDefault();
@ -159,9 +195,50 @@ const CommentSystem = ({ topicId, pbId }) => {
postComment(newComment);
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">
<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 ? (
<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">

View File

@ -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 { Textarea } from '../ui/textarea';
import { token, user_name, pb_id, siliconId, isLogin } from '../../lib/CookieValues';
const CommentThread = ({ comment, depth = 0, onReply }) => {
const isLoginCoockie = JSON.parse(isLogin);
const [isReplying, setIsReplying] = useState(false);
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) => {
e.preventDefault();
@ -14,6 +53,7 @@ const CommentThread = ({ comment, depth = 0, onReply }) => {
onReply(replyText, comment.comment_id);
setReplyText('');
setIsReplying(false);
setShowEmojiPicker(false);
};
return (
@ -23,7 +63,6 @@ const CommentThread = ({ comment, depth = 0, onReply }) => {
>
<div className="bg-[#25272950] rounded-lg shadow-sm px-4 py-3 mb-2">
<div className="flex gap-3 items-center justify-center">
{/* User Avatar SVG */}
<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>
</div>
@ -38,17 +77,71 @@ const CommentThread = ({ comment, depth = 0, onReply }) => {
<p className="text-gray-100 mb-2 whitespace-pre-wrap">{comment.comment_text}</p>
</div>
</div>
{
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>
)
}
{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>
)}
{isReplying && (
<form onSubmit={handleReplySubmit} className="mt-3 pl-11">
<Textarea value={replyText} onChange={(e) => setReplyText(e.target.value)} placeholder="Write your reply..." className="mb-2 min-h-[70px]" required minLength={1} />
<div className="flex gap-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)}>Cancel</Button>
<form onSubmit={handleReplySubmit} className="mt-3 pl-11 relative">
<Textarea
ref={textareaRef}
value={replyText}
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>
</form>
)}

View File

@ -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;
}

View File

@ -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;

View File

@ -353,6 +353,37 @@ const NewTopic = () => {
<Input type="text" name="title" value={formData.title} onChange={handleChange} placeholder="Enter a descriptive title" required />
</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">
<Label htmlFor="slug">URL Slug</Label>
<Input type="text" name="slug" value={formData.slug} onChange={handleChange} readOnly className="bg-gray-50" />
@ -459,37 +490,6 @@ const NewTopic = () => {
</DialogContent>
</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 />
{/* Form Actions */}

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react";
import ImageSlider from "./ImageSlider/ImageSlider"
import { marked } from 'marked';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
import { Label } from "./ui/label";
@ -7,15 +8,29 @@ import { Button } from "./ui/button";
import { useIsLoggedIn } from '../lib/isLoggedIn';
import { token, user_name, pb_id } from '../lib/CookieValues';
import CommentSystem from './CommentSystem/CommentSystem';
const COMMENTS_API_URL = 'https://host-api-sxashuasysagibx.siliconpin.com/v1/comments/';
export default function TopicDetail(props) {
// console.log('coockie data', user_name)
const [showCopied, setShowCopied] = useState(false);
const [allGalleryImages, setAllGalleryImages] = useState([]);
if (!props.topic) {
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 title = props.topic.title;
const text = `Check out this Topic: ${title}`;
@ -195,6 +210,11 @@ export default function TopicDetail(props) {
</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}/>
</article>
</div>

View File

@ -32,6 +32,22 @@ export default function EditTopic (){
const [imageUploadFile, setImageUploadFile] = useState(null);
const [imageUploadPreview, setImageUploadPreview] = useState('');
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
useEffect(() => {
@ -54,7 +70,7 @@ export default function EditTopic (){
title: topic.title || '',
slug: topic.slug || '',
content: topic.content || '',
imageUrl: topic.imageUrl || ''
imageUrl: topic.img || ''
});
} catch (err) {
setError(err.message);
@ -63,8 +79,39 @@ export default function EditTopic (){
}
};
fetchTopic();
fetchTopic(); fetchGalleryImage();
}, [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)
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>
</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 */}
<div className="space-y-2">
<div className="flex justify-between items-center">
@ -496,37 +580,42 @@ export default function EditTopic (){
/>
</DialogContent>
</Dialog>
{/* Featured Image Upload */}
<div className="space-y-2">
<Label htmlFor="image">Featured Image</Label>
{
allGalleryImages && (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 gap-4 mt-4">
{allGalleryImages.map((img, index) => (
<img key={index} src={`https://gallery-image-pocndhvgmcnbacgb.siliconpin.com${img}`} alt={`gallery-${index}`} className="w-full h-auto rounded shadow" />
))}
</div>
)
}
<div>
<Label htmlFor="galleryImage">
Gallery Image: <span className="text-xs text-gray-500">(First write description (Min 10 Char) to proceed image upload!)</span>
</Label>
<div className="flex gap-x-4 mt-1">
<Input
onChange={(e) => setGalleryImgDesc(e.target.value)}
value={galleryImgDesc}
type="text"
placeholder="Write description to proceed image upload!"
/>
<Input
ref={fileInputRef}
onChange={(e) => setGalleryImg(e.target.files[0])}
type="file"
id="image"
onChange={handleFileChange}
accept="image/*"
className="cursor-pointer"
className={`${galleryImgDesc.trim().length > 9 ? 'border-[#6d9e37] file:bg-[#6d9e37] file:text-white file:border-0 file:rounded-md' : ''}`}
disabled={galleryImgDesc.trim().length < 10}
/>
{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>
<Button onClick={() => uploadGalleryImages(galleryImg, slug)} disabled={isSubmitting}>
{isSubmitting ? 'Uploading...' : 'Upload'}
</Button>
</div>
)}
{formData.imageUrl && (
<div className="mt-2">
<img
src={formData.imageUrl}
alt="Preview"
className="max-h-40 rounded-md border"
/>
<p className="text-xs text-gray-500 mt-1">Current Image</p>
</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">&#10003;</span>}
{statusMessage.success === false && <span className="mr-1">&#10060;</span>}
{statusMessage.message}
</p>
</div>
<Separator />

View File

@ -54,7 +54,7 @@ export default function TopicItems(props) {
<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>
<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">
<strong>Date: </strong>
{new Date(topic.timestamp).toLocaleDateString('en-GB', {

View File

@ -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}
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
---
@ -371,32 +371,4 @@ document.addEventListener('DOMContentLoaded', function() {
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>

View File

@ -12,6 +12,7 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
description={pageDescription}
ogImage={pageImage}
type="website"
canonicalURL="/about-us"
>
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
<div class="text-center mb-8 sm:mb-12">

View File

@ -109,6 +109,7 @@ const contactSchema3 = {
description={metaDescription}
ogImage={ogImage}
type="website"
canonicalURL="/contact-us"
newSchema1={JSON.stringify(contactSchema1)}
newSchema2={JSON.stringify(contactSchema2)}
newSchema3={JSON.stringify(contactSchema3)}

View File

@ -19,6 +19,7 @@ const defaultSubdomain = randomString();
description={pageDescription}
ogImage={pageImage}
type="website"
canonicalURL="/get-started"
>
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
<div class="text-center mb-8 sm:mb-12">

View File

@ -107,6 +107,7 @@ const ogImage = "/assets/logo.svg";
newSchema3={JSON.stringify(schema3)}
newSchema4={'here is another schema content'}
newSchema5={'here is another schema content'}
canonicalURL="/"
>
<!-- Canvas background animation -->

View File

@ -12,6 +12,7 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
description={pageDescription}
ogImage={pageImage}
type="website"
canonicslURL="/privacy-policy"
>
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
<div class="text-center mb-8 sm:mb-12">

View File

@ -4,6 +4,6 @@ import Layout from "../../layouts/Layout.astro";
import UserProfile from "../../components/UserProfile";
---
<Layout title="Profile Page">
<Layout title="Profile Page" canonicalURL="/profile">
<UserProfile client:load />
</Layout>

View File

@ -2,6 +2,6 @@
import Layout from "../../layouts/Layout.astro";
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 />
</Layout>

View File

@ -3,6 +3,6 @@ import Layout from "../../layouts/Layout.astro"
import NewHetznerCloudInstance from "../../components/BuyServices/NewHetznerCloudInstance";
---
<Layout title="Cloud Instance | SiliconPin">
<Layout title="Cloud Instance | SiliconPin" canonicalURL="/services/cloud-hetzner-instance">
<NewHetznerCloudInstance client:load />
</Layout>

View File

@ -3,6 +3,6 @@ import Layout from "../../layouts/Layout.astro"
import NewUthoCloudInstance from "../../components/BuyServices/NewUthoCloudInstance";
---
<Layout title="Cloud Instance | SiliconPin">
<Layout title="Cloud Instance | SiliconPin" canonicalURL="/services/cloud-instance">
<NewUthoCloudInstance client:load />
</Layout>

View File

@ -2,6 +2,6 @@
import Layout from "../../layouts/Layout.astro"
import NewDroplet from "../../components/BuyServices/NewDroplet";
---
<Layout title="Create Droplets | SiliconPin">
<Layout title="Create Droplets | SiliconPin" canonicalURL="/services/create-droplets">
<NewDroplet client:load />
</Layout>

View File

@ -231,6 +231,7 @@ const pageImage = "https://images.unsplash.com/photo-1551731409-43eb3e517a1a?q=8
ogImage={pageImage}
type="website"
newSchema1={JSON.stringify(schema1)}
canonicalURL="/services"
>
<main class="container mx-auto px-4 sm:px-6 py-8 sm:py-12">
<div class="text-center mb-8 sm:mb-12">

View File

@ -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>

View File

@ -2,6 +2,6 @@
import Layout from "../../layouts/Layout.astro";
import NewKubernetisUtho from "../../components/BuyServices/NewKubernetisUtho";
---
<Layout title="Deploy Kubernetis | Kubernetis | SiliconPin">
<Layout title="Deploy Kubernetis | Kubernetis | SiliconPin" canonicalURL="/services/kubernetes">
<NewKubernetisUtho client:load />
</Layout>

View File

@ -2,6 +2,6 @@
import Layout from "../../layouts/Layout.astro"
import STTStreaming from "../../components/BuyServices/STTStreaming"
---
<Layout title="STT Streaming | SiliconPin">
<Layout title="STT Streaming | SiliconPin" canonicalURL="/services/stt-streaming">
<STTStreaming client:load />
</Layout>

View File

@ -48,11 +48,36 @@ export async function getStaticPaths() {
// 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} />
</Layout>
<script>
</script>
<style>
#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>

View File

@ -23,6 +23,6 @@ try {
}
---
<Layout title="Topics">
<Layout title="Topics" canonicalURL="/topic">
<TopicsList client:load />
</Layout>

View File

@ -3,6 +3,9 @@ export default {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {
boxShadow: {
xl: '0 10px 30px rgba(0, 0, 0, 0.3)',
},
colors: {
'sp-dark': '#2b323b',
'sp-light': '#c7ccd5',

View File

@ -2382,6 +2382,13 @@ electron-to-chromium@^1.5.73:
"@types/node" "^20.9.0"
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:
version "1.0.0"
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"
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:
version "1.1.1"
resolved "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz"