Imrpoved blogs page

This commit is contained in:
Harivansh Rathi 2024-11-29 11:30:47 -05:00
parent 8fd1ca8260
commit 108b4533e9
17 changed files with 689 additions and 112 deletions

View file

@ -0,0 +1,111 @@
import React, { useState, useEffect } from 'react';
interface Comment {
id: number;
author: string;
content: string;
timestamp: string;
}
interface CommentSectionProps {
postId: number;
}
// Store comments in memory (in a real app, this would be in a database)
const commentsStore: Record<number, Comment[]> = {};
export const CommentSection: React.FC<CommentSectionProps> = ({ postId }) => {
const [comments, setComments] = useState<Comment[]>([]);
const [newComment, setNewComment] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
// Load comments for this post
useEffect(() => {
setComments(commentsStore[postId] || []);
}, [postId]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!newComment.trim()) return;
setIsSubmitting(true);
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 500));
const comment: Comment = {
id: Date.now(),
author: 'Anonymous Farmer', // Replace with actual user system
content: newComment.trim(),
timestamp: new Date().toISOString(),
};
const updatedComments = [comment, ...(commentsStore[postId] || [])];
commentsStore[postId] = updatedComments;
setComments(updatedComments);
setNewComment('');
setIsSubmitting(false);
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
return (
<div className="mt-12">
<h3 className="text-2xl font-bold text-gray-800 mb-6">Comments</h3>
<form onSubmit={handleSubmit} className="mb-8">
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="Share your thoughts..."
className="w-full p-4 border border-gray-200 rounded-lg focus:ring-2 focus:ring-sage-500 focus:border-transparent resize-none"
rows={3}
disabled={isSubmitting}
/>
<button
type="submit"
disabled={isSubmitting || !newComment.trim()}
className={`mt-2 px-6 py-2 rounded-lg text-white transition-colors ${
isSubmitting || !newComment.trim()
? 'bg-gray-400 cursor-not-allowed'
: 'bg-sage-500 hover:bg-sage-600'
}`}
>
{isSubmitting ? 'Posting...' : 'Post Comment'}
</button>
</form>
<div className="space-y-6">
{comments.length === 0 ? (
<p className="text-gray-500 text-center py-8">
No comments yet. Be the first to share your thoughts!
</p>
) : (
comments.map((comment) => (
<div
key={comment.id}
className="bg-white p-6 rounded-lg shadow-sm border border-gray-100"
>
<div className="flex justify-between items-start mb-2">
<span className="font-medium text-gray-800">{comment.author}</span>
<span className="text-sm text-gray-500">
{formatDate(comment.timestamp)}
</span>
</div>
<p className="text-gray-700 whitespace-pre-wrap">{comment.content}</p>
</div>
))
)}
</div>
</div>
);
};

View file

@ -0,0 +1,43 @@
import React from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
interface ErrorBoundaryProps {
children: React.ReactNode;
}
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return (
<div className="min-h-[400px] flex items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-bold text-gray-800 mb-4">
Oops! Something went wrong
</h2>
<button
onClick={() => window.location.reload()}
className="bg-sage-500 text-white px-4 py-2 rounded-lg hover:bg-sage-600 transition-colors"
>
Try Again
</button>
</div>
</div>
);
}
return this.props.children;
}
}

View file

@ -0,0 +1,7 @@
import React from 'react';
export const LoadingSpinner: React.FC = () => (
<div className="flex justify-center items-center min-h-[400px]">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-sage-500 border-t-transparent" />
</div>
);

View file

@ -0,0 +1,59 @@
import React from 'react';
interface PaginationProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}
export const Pagination: React.FC<PaginationProps> = ({
currentPage,
totalPages,
onPageChange,
}) => {
const pages = Array.from({ length: totalPages }, (_, i) => i + 1);
if (totalPages <= 1) return null;
return (
<div className="flex justify-center space-x-2 mt-8">
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
className={`px-4 py-2 rounded-lg ${
currentPage === 1
? 'bg-gray-200 text-gray-500 cursor-not-allowed'
: 'bg-white text-gray-700 hover:bg-sage-100'
}`}
>
Previous
</button>
{pages.map((page) => (
<button
key={page}
onClick={() => onPageChange(page)}
className={`px-4 py-2 rounded-lg ${
currentPage === page
? 'bg-sage-500 text-white'
: 'bg-white text-gray-700 hover:bg-sage-100'
}`}
>
{page}
</button>
))}
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className={`px-4 py-2 rounded-lg ${
currentPage === totalPages
? 'bg-gray-200 text-gray-500 cursor-not-allowed'
: 'bg-white text-gray-700 hover:bg-sage-100'
}`}
>
Next
</button>
</div>
);
};

View file

@ -0,0 +1,60 @@
import React from 'react';
import { BlogPost } from '../data/blogPosts';
interface ShareButtonsProps {
post: BlogPost;
}
export const ShareButtons: React.FC<ShareButtonsProps> = ({ post }) => {
const url = window.location.href;
const title = `Check out ${post.character}'s blog post: ${post.title}`;
const shareLinks = [
{
name: 'Twitter',
url: `https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${encodeURIComponent(url)}`,
icon: (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
</svg>
),
color: 'text-blue-400 hover:text-blue-600',
},
{
name: 'Facebook',
url: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
icon: (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
</svg>
),
color: 'text-blue-600 hover:text-blue-800',
},
{
name: 'Reddit',
url: `https://reddit.com/submit?url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}`,
icon: (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/>
</svg>
),
color: 'text-orange-500 hover:text-orange-700',
},
];
return (
<div className="flex items-center space-x-4 mt-6">
<span className="text-gray-600 font-medium">Share:</span>
{shareLinks.map((link) => (
<button
key={link.name}
onClick={() => window.open(link.url, '_blank')}
className={`${link.color} transition-colors`}
aria-label={`Share on ${link.name}`}
>
{link.icon}
</button>
))}
</div>
);
};