Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
1c6e06bd6d |
|
@ -0,0 +1,371 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import MDEditor, { commands } from '@uiw/react-md-editor';
|
||||||
|
import { Card } from "./ui/card";
|
||||||
|
import { Label } from "./ui/label";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog";
|
||||||
|
import { Input } from "./ui/input";
|
||||||
|
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||||
|
|
||||||
|
const COMMENTS_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/comments/';
|
||||||
|
const MINIO_UPLOAD_URL = 'https://hostapi2.cs1.hz.siliconpin.com/api/storage/upload';
|
||||||
|
|
||||||
|
export default function Comment(props) {
|
||||||
|
const [comments, setComments] = useState([]);
|
||||||
|
const [newComment, setNewComment] = useState({ comment: '' });
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||||
|
const [submitError, setSubmitError] = useState('');
|
||||||
|
const [isLoadingComments, setIsLoadingComments] = useState(true);
|
||||||
|
const [editorMode, setEditorMode] = useState('edit');
|
||||||
|
const [imageDialogOpen, setImageDialogOpen] = useState(false);
|
||||||
|
const [imageUrlInput, setImageUrlInput] = useState('');
|
||||||
|
const [imageUploadFile, setImageUploadFile] = useState(null);
|
||||||
|
const [imageUploadPreview, setImageUploadPreview] = useState('');
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
|
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||||
|
|
||||||
|
// Custom image command for MDEditor
|
||||||
|
const customImageCommand = {
|
||||||
|
name: 'image',
|
||||||
|
keyCommand: 'image',
|
||||||
|
buttonProps: { 'aria-label': 'Insert image' },
|
||||||
|
icon: (
|
||||||
|
<svg width="12" height="12" viewBox="0 0 20 20">
|
||||||
|
<path fill="currentColor" d="M15 9c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm4-7H1c-.55 0-1 .45-1 1v14c0 .55.45 1 1 1h18c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 13l-6-5-2 2-4-5-4 8V4h16v11z"/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
execute: () => {
|
||||||
|
setImageDialogOpen(true);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all default commands and replace the image command
|
||||||
|
const allCommands = commands.getCommands().map(cmd => {
|
||||||
|
if (cmd.name === 'image') {
|
||||||
|
return customImageCommand;
|
||||||
|
}
|
||||||
|
return cmd;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load comments when component mounts
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.topicId) {
|
||||||
|
fetchComments(props.topicId);
|
||||||
|
}
|
||||||
|
}, [props.topicId]);
|
||||||
|
|
||||||
|
const fetchComments = async (topicId) => {
|
||||||
|
setIsLoadingComments(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${COMMENTS_API_URL}?topicId=${topicId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || 'Failed to fetch comments');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success && data.comments) {
|
||||||
|
setComments(data.comments);
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid response format');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching comments:', error);
|
||||||
|
setSubmitError('Failed to load comments');
|
||||||
|
} finally {
|
||||||
|
setIsLoadingComments(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Upload file to MinIO
|
||||||
|
const uploadToMinIO = async (file) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('api_key', 'wweifwehfwfhwhtuyegbvijvbfvegfreyf');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(MINIO_UPLOAD_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Upload failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.publicUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle image URL insertion
|
||||||
|
const handleInsertImageUrl = () => {
|
||||||
|
if (imageUrlInput) {
|
||||||
|
const imgMarkdown = ``;
|
||||||
|
setNewComment(prev => ({
|
||||||
|
...prev,
|
||||||
|
comment: prev.comment ? `${prev.comment}\n${imgMarkdown}` : imgMarkdown
|
||||||
|
}));
|
||||||
|
setImageDialogOpen(false);
|
||||||
|
setImageUrlInput('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle image file selection
|
||||||
|
const handleImageFileSelect = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
setImageUploadFile(file);
|
||||||
|
|
||||||
|
// Create preview
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
setImageUploadPreview(reader.result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Upload image file to MinIO and insert into editor
|
||||||
|
const handleImageUpload = async () => {
|
||||||
|
if (!imageUploadFile) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setUploadProgress(0);
|
||||||
|
|
||||||
|
const uploadedUrl = await uploadToMinIO(imageUploadFile);
|
||||||
|
|
||||||
|
// Insert markdown for the uploaded image
|
||||||
|
const imgMarkdown = ``;
|
||||||
|
setNewComment(prev => ({
|
||||||
|
...prev,
|
||||||
|
comment: prev.comment ? `${prev.comment}\n${imgMarkdown}` : imgMarkdown
|
||||||
|
}));
|
||||||
|
|
||||||
|
setImageDialogOpen(false);
|
||||||
|
setImageUploadFile(null);
|
||||||
|
setImageUploadPreview('');
|
||||||
|
} catch (error) {
|
||||||
|
setSubmitError('Failed to upload image: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitComment = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setSubmitError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${COMMENTS_API_URL}?query=new-comment`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...newComment,
|
||||||
|
topicId: props.topicId
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setNewComment({ comment: '' });
|
||||||
|
setSubmitSuccess(true);
|
||||||
|
setTimeout(() => setSubmitSuccess(false), 3000);
|
||||||
|
|
||||||
|
// Refresh comments after successful submission
|
||||||
|
await fetchComments(props.topicId);
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
setSubmitError(errorData.message || 'Failed to submit comment');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setSubmitError('Network error. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserInitials = (name) => {
|
||||||
|
if (!name) return 'U';
|
||||||
|
const words = name.trim().split(' ');
|
||||||
|
return words
|
||||||
|
.slice(0, 2)
|
||||||
|
.map(word => word[0].toUpperCase())
|
||||||
|
.join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Comments List */}
|
||||||
|
<div className="space-y-6 border-t">
|
||||||
|
<h2 className="text-2xl font-bold text-[#6d9e37]">Comments ({comments.length})</h2>
|
||||||
|
|
||||||
|
{isLoadingComments ? (
|
||||||
|
<div className="flex justify-center py-4">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[#6d9e37]"></div>
|
||||||
|
</div>
|
||||||
|
) : comments.length === 0 ? (
|
||||||
|
<p className="text-gray-500">No comments yet. Be the first to comment!</p>
|
||||||
|
) : (
|
||||||
|
comments.map(comment => (
|
||||||
|
<div key={comment.id} className="border-b pb-6 last:border-b-0">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 font-bold">
|
||||||
|
{getUserInitials(comment.userName)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h4 className="font-medium text-[#6d9e37]">
|
||||||
|
{comment.userName || 'User'}
|
||||||
|
</h4>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{new Date(comment.created_at).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div data-color-mode="light" className="markdown-body">
|
||||||
|
<MDEditor.Markdown source={comment.comment} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Comments Section with MDEditor */}
|
||||||
|
<Card className="mt-16 border-t">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="px-6 pb-6 rounded-lg">
|
||||||
|
<h3 className="text-lg font-medium mb-4 pt-8">Leave a Comment</h3>
|
||||||
|
{submitSuccess && (
|
||||||
|
<div className="mb-4 p-3 bg-green-100 text-green-700 rounded">
|
||||||
|
Thank you for your comment!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{submitError && (
|
||||||
|
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||||
|
{submitError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<form onSubmit={handleSubmitComment}>
|
||||||
|
<div className="mb-4">
|
||||||
|
<Label htmlFor="comment">Comment *</Label>
|
||||||
|
<div data-color-mode="light">
|
||||||
|
<MDEditor
|
||||||
|
placeholder="Write your comment (markdown supported)"
|
||||||
|
value={newComment.comment}
|
||||||
|
onChange={(value) => setNewComment(prev => ({ ...prev, comment: value || '' }))}
|
||||||
|
height={300}
|
||||||
|
preview={editorMode}
|
||||||
|
commands={allCommands}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end mt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setEditorMode(editorMode === 'edit' ? 'preview' : 'edit')}
|
||||||
|
className={`text-sm ${editorMode !== 'edit' ? 'bg-[#6d9e37] text-white' : 'text-[#6d9e37]'} px-2 py-1 rounded-md border border-[#6d9e37]`}
|
||||||
|
>
|
||||||
|
{editorMode === 'edit' ? 'Preview' : 'Edit'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" disabled={isSubmitting || !isLoggedIn}>
|
||||||
|
{isSubmitting ? 'Submitting...' : 'Post Comment'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image Upload Dialog */}
|
||||||
|
<Dialog open={imageDialogOpen} onOpenChange={setImageDialogOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Insert Image</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="image-url">From URL</Label>
|
||||||
|
<Input
|
||||||
|
id="image-url"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter image URL"
|
||||||
|
value={imageUrlInput}
|
||||||
|
onChange={(e) => setImageUrlInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleInsertImageUrl}
|
||||||
|
disabled={!imageUrlInput}
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
Insert Image
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="image-upload">Upload Image</Label>
|
||||||
|
<Input
|
||||||
|
id="image-upload"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleImageFileSelect}
|
||||||
|
/>
|
||||||
|
{imageUploadPreview && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<img
|
||||||
|
src={imageUploadPreview}
|
||||||
|
alt="Preview"
|
||||||
|
className="max-h-40 rounded-md border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleImageUpload}
|
||||||
|
disabled={!imageUploadFile || isSubmitting}
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Uploading...' : 'Upload & Insert'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<div className="w-full h-full absolute bg-black inset-0 opacity-70 backdrop-blur-2xl rounded-lg" />
|
||||||
|
<div className="w-10 h-10 rounded-full border-2 border-dotted border-[#6d9e37] absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" role="status">
|
||||||
|
<span className="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : !isLoggedIn ? (
|
||||||
|
<>
|
||||||
|
<div className="w-full h-full absolute bg-black inset-0 opacity-70 backdrop-blur-2xl rounded-lg" />
|
||||||
|
<p className="text-gray-100 bg-gray-700 p-2 rounded-md shadow-xl italic absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||||
|
Join the conversation! Log in or sign up to post a comment
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,200 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Card } from "./ui/card";
|
||||||
|
import { Label } from "./ui/label";
|
||||||
|
import { Textarea } from "./ui/textarea";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||||
|
|
||||||
|
const COMMENTS_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/comments/';
|
||||||
|
|
||||||
|
export default function Comment(props) {
|
||||||
|
const [comments, setComments] = useState([]);
|
||||||
|
const [newComment, setNewComment] = useState({ comment: '' });
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [submitSuccess, setSubmitSuccess] = useState(false);
|
||||||
|
const [submitError, setSubmitError] = useState('');
|
||||||
|
const [isLoadingComments, setIsLoadingComments] = useState(true);
|
||||||
|
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
||||||
|
|
||||||
|
// Load comments when component mounts or when topicId changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.topicId) {
|
||||||
|
fetchComments(props.topicId);
|
||||||
|
}
|
||||||
|
}, [props.topicId]);
|
||||||
|
|
||||||
|
const fetchComments = async (topicId) => {
|
||||||
|
setIsLoadingComments(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${COMMENTS_API_URL}?topicId=${topicId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || 'Failed to fetch comments');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success && data.comments) {
|
||||||
|
setComments(data.comments);
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid response format');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching comments:', error);
|
||||||
|
setSubmitError('Failed to load comments');
|
||||||
|
} finally {
|
||||||
|
setIsLoadingComments(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setNewComment(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitComment = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setSubmitError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${COMMENTS_API_URL}?query=new-comment`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...newComment,
|
||||||
|
topicId: props.topicId
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setNewComment({ comment: '' });
|
||||||
|
setSubmitSuccess(true);
|
||||||
|
setTimeout(() => setSubmitSuccess(false), 3000);
|
||||||
|
|
||||||
|
// Refresh comments after successful submission
|
||||||
|
await fetchComments(props.topicId);
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
setSubmitError(errorData.message || 'Failed to submit comment');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setSubmitError('Network error. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserInitials = (name) => {
|
||||||
|
if (!name) return 'U';
|
||||||
|
const words = name.trim().split(' ');
|
||||||
|
return words
|
||||||
|
.slice(0, 2)
|
||||||
|
.map(word => word[0].toUpperCase())
|
||||||
|
.join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Comments List */}
|
||||||
|
<div className="space-y-6 border-t">
|
||||||
|
<h2 className="text-2xl font-bold text-[#6d9e37]">Comments ({comments.length})</h2>
|
||||||
|
|
||||||
|
{isLoadingComments ? (
|
||||||
|
<div className="flex justify-center py-4">
|
||||||
|
{/* <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[#6d9e37]"></div> */}
|
||||||
|
</div>
|
||||||
|
) : comments.length === 0 ? (
|
||||||
|
<p className="text-gray-500">No comments yet. Be the first to comment!</p>
|
||||||
|
) : (
|
||||||
|
comments.map(comment => (
|
||||||
|
<div key={comment.id} className="border-b pb-6 last:border-b-0">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 font-bold">
|
||||||
|
{getUserInitials(comment.userName)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h4 className="font-medium text-[#6d9e37]">
|
||||||
|
{comment.userName || 'User'}
|
||||||
|
</h4>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{new Date(comment.created_at).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="">{comment.comment}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Comments Section */}
|
||||||
|
<Card className="mt-16 border-t">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="px-6 pb-6 rounded-lg">
|
||||||
|
<h3 className="text-lg font-medium mb-4 pt-8">Leave a Comment</h3>
|
||||||
|
{submitSuccess && (
|
||||||
|
<div className="mb-4 p-3 bg-green-100 text-green-700 rounded">
|
||||||
|
Thank you for your comment!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{submitError && (
|
||||||
|
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
||||||
|
{submitError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<form onSubmit={handleSubmitComment}>
|
||||||
|
<div className="mb-4">
|
||||||
|
<Label htmlFor="comment">Comment *</Label>
|
||||||
|
<Textarea
|
||||||
|
className="mt-2"
|
||||||
|
id="comment"
|
||||||
|
name="comment"
|
||||||
|
rows="4"
|
||||||
|
value={newComment.comment}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
disabled={!isLoggedIn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" disabled={isSubmitting || !isLoggedIn}>
|
||||||
|
{isSubmitting ? 'Submitting...' : 'Post Comment'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<div className="w-full h-full absolute bg-black inset-0 opacity-70 backdrop-blur-2xl rounded-lg" />
|
||||||
|
<div className="w-10 h-10 rounded-full border-2 border-dotted border-[#6d9e37] absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" role="status">
|
||||||
|
<span className="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : !isLoggedIn ? (
|
||||||
|
<>
|
||||||
|
<div className="w-full h-full absolute bg-black inset-0 opacity-70 backdrop-blur-2xl rounded-lg" />
|
||||||
|
<p className="text-gray-100 bg-gray-700 p-2 rounded-md shadow-xl italic absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||||
|
Join the conversation! Log in or sign up to post a comment
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,9 +1,15 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
|
||||||
|
import { Label } from "./ui/label";
|
||||||
|
import { Textarea } from "./ui/textarea";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { useIsLoggedIn } from '../lib/isLoggedIn';
|
||||||
|
import Comment from './Comment';
|
||||||
|
const COMMENTS_API_URL = 'https://host-api.cs1.hz.siliconpin.com/v1/comments/';
|
||||||
export default function TopicDetail(props) {
|
export default function TopicDetail(props) {
|
||||||
const [showCopied, setShowCopied] = useState(false);
|
const [showCopied, setShowCopied] = useState(false);
|
||||||
|
|
||||||
if (!props.topic) {
|
if (!props.topic) {
|
||||||
return <div>Topic not found</div>;
|
return <div>Topic not found</div>;
|
||||||
}
|
}
|
||||||
|
@ -12,31 +18,6 @@ export default function TopicDetail(props) {
|
||||||
const title = props.topic.title;
|
const title = props.topic.title;
|
||||||
const text = `Check out this article: ${title}`;
|
const text = `Check out this article: ${title}`;
|
||||||
|
|
||||||
// const shareOnFacebook = () => {
|
|
||||||
// window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`, '_blank');
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const shareOnTwitter = () => {
|
|
||||||
// window.open(`https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(text)}`, '_blank');
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const shareOnLinkedIn = () => {
|
|
||||||
// window.open(`https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(title)}`, '_blank');
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const shareOnReddit = () => {
|
|
||||||
// window.open(`https://www.reddit.com/submit?url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(title)}`, '_blank');
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const shareOnWhatsApp = () => {
|
|
||||||
// window.open(`https://wa.me/?text=${encodeURIComponent(`${text} ${shareUrl}`)}`, '_blank');
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const shareViaEmail = () => {
|
|
||||||
// window.open(`mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(`${text}\n\n${shareUrl}`)}`);
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
|
||||||
const shareOnSocialMedia = (platform) => {
|
const shareOnSocialMedia = (platform) => {
|
||||||
switch (platform){
|
switch (platform){
|
||||||
case 'facebook':
|
case 'facebook':
|
||||||
|
@ -125,7 +106,7 @@ export default function TopicDetail(props) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-12">
|
<div className="container mx-auto px-4 py-12">
|
||||||
<article className="max-w-4xl mx-auto">
|
<article className="max-w-4xl mx-auto">
|
||||||
<img src={props.topic.img ? props.topic.img : '/assets/images/thumb-place.jpg'} alt={props.topic.title} className="w-full h-[400px] aspect-video object-cover rounded-lg mb-8 shadow-md" />
|
<img src={props.topic.img ? props.topic.img : '/assets/images/thumb-place.jpg'} alt={props.topic.title} className="w-full h-[400px] aspect-video object-cover rounded-lg mb-8 shadow-md" />
|
||||||
<h1 className="text-4xl font-bold text-[#6d9e37] mb-6">{props.topic.title}</h1>
|
<h1 className="text-4xl font-bold text-[#6d9e37] mb-6">{props.topic.title}</h1>
|
||||||
|
|
||||||
{/* Enhanced Social Share Buttons */}
|
{/* Enhanced Social Share Buttons */}
|
||||||
|
@ -211,11 +192,11 @@ export default function TopicDetail(props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="font-light mb-8 text-justify prose max-w-none" dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }} ></div>
|
||||||
className="font-light mb-8 text-justify prose max-w-none"
|
<Comment topicId={props.topic.id}/>
|
||||||
dangerouslySetInnerHTML={{ __html: marked.parse(props.topic.content || '') }}
|
|
||||||
></div>
|
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bg-[#6d9e37]
|
Loading…
Reference in New Issue