diff --git a/client/src/App.tsx b/client/src/App.tsx index dc00a55b..b4862034 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Routes, Route } from "react-router-dom"; -import HomePage from "./components/HomePage"; +import HomePage from "./components/feed/HomePage"; import Login from "./components/authentication/Login"; import SignUp from "./components/authentication/SignUp"; diff --git a/client/src/components/HomePage.tsx b/client/src/components/feed/HomePage.tsx similarity index 51% rename from client/src/components/HomePage.tsx rename to client/src/components/feed/HomePage.tsx index 25d0422c..3afff6df 100644 --- a/client/src/components/HomePage.tsx +++ b/client/src/components/feed/HomePage.tsx @@ -1,23 +1,46 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import Button from "@mui/material/Button"; import Grid from "@mui/material/Grid"; import Typography from "@mui/material/Typography"; import { Box, CssBaseline, Paper } from "@mui/material"; import AppBar from "@mui/material/AppBar"; -import MakePostModal from "./post/MakePostModal"; +import MakePostModal from "../post/MakePostModal"; +import PostsList from "../post/PostsList"; +import axios from "axios"; +import {Post} from "../../interfaces/interfaces"; + +const APP_URI = process.env.REACT_APP_URI; export default function HomePage() { const [isMakePostModalOpen, setIsMakePostModalOpen] = useState(false); + const [posts, setPosts] = useState([]); const openMakePostModal = () => { setIsMakePostModalOpen(true); }; + const fetchPosts = async () => { + // TODO: replace hardcoded author id with AUTHOR_ID + const url = `${APP_URI}author/5ba6d758-257f-4f47-b0b7-d3d5f5e32561/posts/`; + + try { + const response = await axios.get(url); + setPosts(response.data); + } catch (error) { + console.error("Error fetching posts:", error); + } + }; + + // https://react.dev/reference/react/useEffect + useEffect(() => { + fetchPosts(); + }, []); + return ( <> - + - + - + - - main + + + + + side diff --git a/client/src/components/post/MakePostModal.tsx b/client/src/components/post/MakePostModal.tsx index 7934f6b6..ecbf02e3 100644 --- a/client/src/components/post/MakePostModal.tsx +++ b/client/src/components/post/MakePostModal.tsx @@ -23,9 +23,11 @@ const APP_URI = process.env.REACT_APP_URI; const MakePostModal = ({ isModalOpen, + onPostCreated, setIsModalOpen, }: { isModalOpen: boolean; + onPostCreated: () => void; setIsModalOpen: (isOpen: boolean) => void; }) => { const [title, setTitle] = useState(""); @@ -55,6 +57,7 @@ const MakePostModal = ({ try { await axios.post(url, payload); + onPostCreated(); handleClose(); } catch (error) { console.error("Failed to post", error); diff --git a/client/src/components/post/PostsList.tsx b/client/src/components/post/PostsList.tsx new file mode 100644 index 00000000..3343e5bb --- /dev/null +++ b/client/src/components/post/PostsList.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Post } from "../../interfaces/interfaces"; +import { Avatar, Card, CardContent, CardHeader, Typography } from "@mui/material"; +import { theme } from "../../index"; +import { formatDateTime } from "../../utils/dateUtils"; + +const PostsList = ({ + posts, +}: { + posts: Post[]; +}) => { + return ( + <> + { posts.map(post => ( + + + {post.author.displayName[0]} + + } + title={post.author.displayName} + subheader={formatDateTime(post.published)} + sx = {{marginTop:2}} + /> + + {post.title} + {post.description} + {post.content} + + + ))} + + ); +}; + +export default PostsList; \ No newline at end of file diff --git a/client/src/enums/enums.tsx b/client/src/enums/enums.tsx new file mode 100644 index 00000000..d7fd6573 --- /dev/null +++ b/client/src/enums/enums.tsx @@ -0,0 +1,7 @@ +export enum ContentType { + BASE64 = "application/base64", + JPEG = "image/jpeg;base64", + MARKDOWN = "text/markdown", + PLAIN = "text/plain", + PNG = "image/png;base64", +} diff --git a/client/src/index.tsx b/client/src/index.tsx index 49361d64..756ab925 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -11,7 +11,7 @@ import "./index.css"; import 'react-toastify/dist/ReactToastify.css'; -const theme = createTheme({ +export const theme = createTheme({ palette: { primary: { main: "#103f5b", diff --git a/client/src/interfaces/interfaces.tsx b/client/src/interfaces/interfaces.tsx new file mode 100644 index 00000000..5632b576 --- /dev/null +++ b/client/src/interfaces/interfaces.tsx @@ -0,0 +1,31 @@ +import { ContentType } from "../enums/enums"; + +export interface Author { + id: string; + createdAt: string; + displayName: string; + github?: string | null; + host: string; + profileImage: string; + url: string; +} + +export interface Category { + category: string; +} + +export interface Post { + id: string; + author: Author; + categories: Category[]; + content?: string | null; + contentType: ContentType; + description: string; + title: string; + source: string; + origin: string; + published: string; + updatedAt?: string | null; + visibility: string; + unlisted: boolean; +} diff --git a/client/src/utils/dateUtils.tsx b/client/src/utils/dateUtils.tsx new file mode 100644 index 00000000..ce48a143 --- /dev/null +++ b/client/src/utils/dateUtils.tsx @@ -0,0 +1,17 @@ +export function formatDateTime(date_str: string): string { + // TypeScript Date Object: https://www.javatpoint.com/typescript-date-object + const date = new Date(date_str); + const months = [ + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ]; + + const hours = date.getHours(); + const minutes = String(date.getMinutes()).padStart(2, '0'); + + // Convert 24-hour format to 12-hour format and determine AM or PM + const twelveHour = hours % 12 || 12; + const amOrPm = hours < 12 ? "AM" : "PM"; + + return `${twelveHour}:${minutes} ${amOrPm}, ${months[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`; +} diff --git a/server/socialdistribution/models/post.py b/server/socialdistribution/models/post.py index 9018fe6e..cc7251ee 100644 --- a/server/socialdistribution/models/post.py +++ b/server/socialdistribution/models/post.py @@ -10,11 +10,11 @@ class Post(models.Model): """ # Types of content types class ContentType(models.TextChoices): - BASE64 = 'application/base64' # images can need base 64 decoding - JPEG = 'image/jpeg;base64' - MARKDOWN = 'text/markdown' - PLAIN = 'text/plain' - PNG = 'image/png;base64' + BASE64 = "application/base64" # images can need base 64 decoding + JPEG = "image/jpeg;base64" + MARKDOWN = "text/markdown" + PLAIN = "text/plain" + PNG = "image/png;base64" # Types of visibility for posts class Visibility(models.TextChoices): diff --git a/server/socialdistribution/views.py b/server/socialdistribution/views.py index cdeb1c5d..38c48fcc 100644 --- a/server/socialdistribution/views.py +++ b/server/socialdistribution/views.py @@ -29,7 +29,7 @@ def get(self, request, author_id): get the recent posts from author AUTHOR_ID TODO: paginate """ - posts = Post.objects.filter(author__id=author_id) + posts = Post.objects.filter(author__id=author_id).order_by("-published") serializer = PostSerializer(posts, many=True, context={"request": request}) return Response(serializer.data, status=status.HTTP_200_OK)