Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions bs3/src/components/comments-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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<Comment[]>([]);
const [text, setText] = useState('');

const textAreaRef = useRef<HTMLTextAreaElement | null>(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<HTMLFormElement>) => {
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 (
<div>
<Typography variant="h6" sx={{ mb: 1, ml: 1 }}>
Comments ({comments.length})
</Typography>
<ul>
{comments.map(({ id, text, timestamp }) => (
<Card key={id} sx={{ my: 0.5 }} elevation={0}>
<CardContent sx={{ py: 0.5 }}>
<Typography variant="body1">{text}</Typography>
<Typography variant="caption" color="text.secondary">
{timeAgo(timestamp)}
</Typography>
</CardContent>
</Card>
))}
{location.local && (
<Card sx={{ my: 0.5 }} elevation={0}>
<CardContent sx={{ py: 0.5, mb: 4 }}>
<form onSubmit={handleSubmit}>
<TextField
id="comment"
label="Add a comment..."
value={text}
onChange={(e) => setText(e.target.value)}
inputRef={textAreaRef}
multiline
minRows={1}
required
fullWidth
sx={{ mb: 1 }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Fab
color="secondary"
aria-label="send"
size="small"
type="submit"
disabled={!text.trim()}>
<SendIcon />
</Fab>
</InputAdornment>
),
}}
/>
</form>
</CardContent>
</Card>
)}
</ul>
</div>
);
}

export default CommentsSection;
62 changes: 35 additions & 27 deletions bs3/src/components/post-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -46,11 +51,11 @@ interface ThisPostState {
}

export default function BasicCard({ post, extended = false }: BasicCardProps) {

const dispatch = useAppDispatch();
const navigate = useNavigate();

const [deleteWarningOpen, setDeleteWarningOpen] = useState<boolean>(false);
const [commentsCount, setCommentsCount] = useState<number>(0);

const userId = useAppSelector(state => state.app.user);
const location = useAppSelector(state => state.geolocation.location);
Expand All @@ -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');
Expand Down Expand Up @@ -171,9 +185,15 @@ export default function BasicCard({ post, extended = false }: BasicCardProps) {
</div>
)}
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Typography variant="body2" color="text.secondary" gutterBottom>
{timeAgo(post.timestamp)}
</Typography>
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2" color="text.secondary" gutterBottom>
{commentsCount}
</Typography>
<ChatBubbleIcon fontSize="small" sx={{color: 'gray'}}/>
<Typography variant="body2" color="text.secondary" gutterBottom>
{timeAgo(post.timestamp)}
</Typography>
</Stack>
</Box>
<Stack
direction="row"
Expand Down Expand Up @@ -248,31 +268,19 @@ export default function BasicCard({ post, extended = false }: BasicCardProps) {
<Typography variant="h5" component="div">
{post.title}
</Typography>
<ReactMarkdown>{post.body}</ReactMarkdown>
<Typography component="div">
<ReactMarkdown>{post.body}</ReactMarkdown>
</Typography>
</CardActionArea>
</ConditionalLink>
</Stack>
</CardContent>
{extended && (
<>
<Divider sx={{ my: 2 }} />
<CommentsSection postId={post.id} />
</>
)}
</Card>
);
}

// 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`;
}
}
19 changes: 19 additions & 0 deletions bs3/src/components/utils.ts
Original file line number Diff line number Diff line change
@@ -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`;
}
}
10 changes: 10 additions & 0 deletions bs3/src/types/types.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down