From a1067c0b9526f5ef5f469235850d717791c14ff8 Mon Sep 17 00:00:00 2001 From: Roman Popat Date: Sun, 13 Apr 2025 06:58:20 +0000 Subject: [PATCH 1/2] comments section MVP --- bs3/src/components/comments-section.tsx | 132 ++++++++++++++++++++++++ bs3/src/components/post-card.tsx | 62 ++++++----- bs3/src/components/utils.ts | 19 ++++ bs3/src/types/types.ts | 10 ++ 4 files changed, 196 insertions(+), 27 deletions(-) create mode 100644 bs3/src/components/comments-section.tsx create mode 100644 bs3/src/components/utils.ts diff --git a/bs3/src/components/comments-section.tsx b/bs3/src/components/comments-section.tsx new file mode 100644 index 0000000..026719c --- /dev/null +++ b/bs3/src/components/comments-section.tsx @@ -0,0 +1,132 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { db } from '../api/firebaseConfig'; +import { + collection, + addDoc, + onSnapshot, + query, + orderBy, + updateDoc, +} from 'firebase/firestore'; +import { Card, CardContent, Typography, TextField, InputAdornment, Fab } from '@mui/material'; +import SendIcon from '@mui/icons-material/Send'; +import { Comment } from '../types/types'; +import { useAppSelector } from '../app/hooks'; +import { timeAgo } from './utils'; + +interface CommentsSectionProps { + postId: string; +} + +function CommentsSection({ postId }: CommentsSectionProps) { + + const [comments, setComments] = useState([]); + const [text, setText] = useState(''); + + const [focused, setFocused] = useState(false); + + const textAreaRef = useRef(null); + + const userId = useAppSelector(state => state.app.user); + const location = useAppSelector(state => state.geolocation.location); + + useEffect(() => { + if (textAreaRef.current) { + // Scroll to the bottom of the TextField's container + const { top, height } = textAreaRef.current.getBoundingClientRect(); + const scrollPosition = window.scrollY + top + height; + window.scrollTo({ top: scrollPosition, behavior: "smooth" }); + } + }, [text]); // Trigger on value changes + + + useEffect(() => { + const commentsRef = collection(db, 'posts', postId, 'comments'); + const q = query(commentsRef, orderBy('timestamp', 'asc')); + + const unsubscribe = onSnapshot(q, (snapshot) => { + const newComments = snapshot.docs.map((doc) => ({ + ...(doc.data() as Comment), + })); + setComments(newComments); + }); + + return () => unsubscribe(); + }, [postId]); + + const handleSubmit = async (e: React.FormEvent) => { + console.log("handleSubmit called"); + e.preventDefault(); + const commentsRef = collection(db, 'posts', postId, 'comments'); + const newComment = await addDoc(commentsRef, { + // author: string; + text, + upVoted: [], + downVoted: [], + timestamp: new Date().toISOString(), + userId: userId, + }); + await updateDoc(newComment, { + id: newComment.id, // Update the document with its own ID + }); + setText(''); + }; + + return ( +
+ + Comments ({comments.length}) + +
    + {comments.map(({ id, text, timestamp }) => ( + + + {text} + + {timeAgo(timestamp)} + + + + ))} + {location.local && ( + + +
    + setText(e.target.value)} + inputRef={textAreaRef} + multiline + minRows={1} + required + fullWidth + sx={{ mb: 1 }} + onFocus={() => setFocused(true)} + onBlur={() => setFocused(false)} + InputProps={{ + endAdornment: ( + + + + + + ), + }} + /> + +
    +
    + )} +
+
+ ); +} + +export default CommentsSection; \ No newline at end of file diff --git a/bs3/src/components/post-card.tsx b/bs3/src/components/post-card.tsx index 6637bc3..dc7f2b7 100644 --- a/bs3/src/components/post-card.tsx +++ b/bs3/src/components/post-card.tsx @@ -5,17 +5,22 @@ import CardContent from '@mui/material/CardContent'; import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; import Chip from '@mui/material/Chip'; -import { CardActionArea, Stack, IconButton, Modal, Box } from '@mui/material'; +import { CardActionArea, Stack, IconButton, Modal, Box, Divider } from '@mui/material'; import ArrowCircleDownIcon from '@mui/icons-material/ArrowCircleDown'; import ArrowCircleUpIcon from '@mui/icons-material/ArrowCircleUp'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; +import ChatBubbleIcon from '@mui/icons-material/ChatBubble'; import { Link, useNavigate } from 'react-router-dom'; import { useAppDispatch, useAppSelector } from '../app/hooks'; import { upVotePost, downVotePost, sortPosts, deletePost } from '../app/firestoreSlice'; -import { useState } from 'react' +import { useState, useEffect } from 'react'; import ReactMarkdown from 'react-markdown'; import { PostData } from '../types/types'; +import CommentsSection from './comments-section'; +import { timeAgo } from './utils'; +import { collection, onSnapshot, query } from 'firebase/firestore'; +import { db } from '../api/firebaseConfig'; // adjust import if needed // Define types for ConditionalLink props interface ConditionalLinkProps { @@ -46,11 +51,11 @@ interface ThisPostState { } export default function BasicCard({ post, extended = false }: BasicCardProps) { - const dispatch = useAppDispatch(); const navigate = useNavigate(); const [deleteWarningOpen, setDeleteWarningOpen] = useState(false); + const [commentsCount, setCommentsCount] = useState(0); const userId = useAppSelector(state => state.app.user); const location = useAppSelector(state => state.geolocation.location); @@ -60,6 +65,15 @@ export default function BasicCard({ post, extended = false }: BasicCardProps) { userDownVoted: post.downVoted.includes(userId), }); + useEffect(() => { + const commentsRef = collection(db, 'posts', post.id, 'comments'); + const q = query(commentsRef); + const unsub = onSnapshot(q, (snapshot) => { + setCommentsCount(snapshot.size); + }); + return () => unsub(); + }, [post.id]); + const handleEdit = () => { localStorage.setItem('newPostForm', JSON.stringify(post)); navigate('/new'); @@ -171,9 +185,15 @@ export default function BasicCard({ post, extended = false }: BasicCardProps) { )} - - {timeAgo(post.timestamp)} - + + + {commentsCount} + + + + {timeAgo(post.timestamp)} + + {post.title} - {post.body} + + {post.body} + + {extended && ( + <> + + + + )} ); -} - -// Helper function for timeAgo -function timeAgo(timestamp: string): string { - const now = new Date(); - const postDate = new Date(timestamp); - const diffInSeconds = Math.floor((now.getTime() - postDate.getTime()) / 1000); - - if (diffInSeconds < 60) { - return 'now'; - } else if (diffInSeconds < 3600) { - const minutes = Math.floor(diffInSeconds / 60); - return `${minutes} minute${minutes > 1 ? 's' : ''} ago`; - } else if (diffInSeconds < 86400) { - const hours = Math.floor(diffInSeconds / 3600); - return `${hours} hour${hours > 1 ? 's' : ''} ago`; - } else { - const days = Math.floor(diffInSeconds / 86400); - return `${days} day${days > 1 ? 's' : ''} ago`; - } } \ No newline at end of file diff --git a/bs3/src/components/utils.ts b/bs3/src/components/utils.ts new file mode 100644 index 0000000..93f9093 --- /dev/null +++ b/bs3/src/components/utils.ts @@ -0,0 +1,19 @@ +// Helper function for timeAgo +export function timeAgo(timestamp: string): string { + const now = new Date(); + const postDate = new Date(timestamp); + const diffInSeconds = Math.floor((now.getTime() - postDate.getTime()) / 1000); + + if (diffInSeconds < 60) { + return 'now'; + } else if (diffInSeconds < 3600) { + const minutes = Math.floor(diffInSeconds / 60); + return `${minutes} minute${minutes > 1 ? 's' : ''} ago`; + } else if (diffInSeconds < 86400) { + const hours = Math.floor(diffInSeconds / 3600); + return `${hours} hour${hours > 1 ? 's' : ''} ago`; + } else { + const days = Math.floor(diffInSeconds / 86400); + return `${days} day${days > 1 ? 's' : ''} ago`; + } + } \ No newline at end of file diff --git a/bs3/src/types/types.ts b/bs3/src/types/types.ts index ed07a26..cad9d67 100644 --- a/bs3/src/types/types.ts +++ b/bs3/src/types/types.ts @@ -1,3 +1,13 @@ +export type Comment = { + id: string; + author: string; + text: string; + upVoted: string[]; + downVoted: string[]; + timestamp: string; + userId: string; +} + export type PostData = { id: string; title: string; From 9fbea831f656a5175baa897a9cc574594fa74ade Mon Sep 17 00:00:00 2001 From: Roman Popat Date: Sun, 13 Apr 2025 07:03:39 +0000 Subject: [PATCH 2/2] refactor: remove unused focus state management in comments section --- bs3/src/components/comments-section.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bs3/src/components/comments-section.tsx b/bs3/src/components/comments-section.tsx index 026719c..7b8ef1d 100644 --- a/bs3/src/components/comments-section.tsx +++ b/bs3/src/components/comments-section.tsx @@ -23,8 +23,6 @@ function CommentsSection({ postId }: CommentsSectionProps) { const [comments, setComments] = useState([]); const [text, setText] = useState(''); - const [focused, setFocused] = useState(false); - const textAreaRef = useRef(null); const userId = useAppSelector(state => state.app.user); @@ -103,8 +101,6 @@ function CommentsSection({ postId }: CommentsSectionProps) { required fullWidth sx={{ mb: 1 }} - onFocus={() => setFocused(true)} - onBlur={() => setFocused(false)} InputProps={{ endAdornment: (