diff --git a/backend/src/models/article.ts b/backend/src/models/article.ts index d94c8bf..f117a96 100644 --- a/backend/src/models/article.ts +++ b/backend/src/models/article.ts @@ -14,7 +14,7 @@ const articleSchema = new Schema({ body: { type: String }, // File URL to article image - thumbnail: { type: String }, + thumbnail: { type: String, required: true }, }); type ArticleItem = InferSchemaType; diff --git a/backend/src/validators/article.ts b/backend/src/validators/article.ts index aa3e152..a795e50 100644 --- a/backend/src/validators/article.ts +++ b/backend/src/validators/article.ts @@ -37,7 +37,11 @@ const makeBodyValidator = () => body("body").optional().isString().withMessage("body must be a string"); const makeThumbnailValidator = () => - body("thumbnail").optional().isURL().withMessage("thumbnail must be a url"); + body("thumbnail") + .exists() + .withMessage("thumbnail is required") + .isURL() + .withMessage("thumbnail must be a url"); export const createArticle = [ makeHeaderValidator(), diff --git a/frontend/public/fonts/LibreBaskerville-Regular.ttf b/frontend/public/fonts/LibreBaskerville-Regular.ttf new file mode 100644 index 0000000..8b87139 Binary files /dev/null and b/frontend/public/fonts/LibreBaskerville-Regular.ttf differ diff --git a/frontend/public/icons/backArrow.svg b/frontend/public/icons/backArrow.svg new file mode 100644 index 0000000..3ef8cbe --- /dev/null +++ b/frontend/public/icons/backArrow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/icons/learn-more.svg b/frontend/public/icons/learnMore.svg similarity index 100% rename from frontend/public/icons/learn-more.svg rename to frontend/public/icons/learnMore.svg diff --git a/frontend/public/icons/left-arrow.svg b/frontend/public/icons/leftArrow.svg similarity index 100% rename from frontend/public/icons/left-arrow.svg rename to frontend/public/icons/leftArrow.svg diff --git a/frontend/public/icons/right-arrow.svg b/frontend/public/icons/rightArrow.svg similarity index 100% rename from frontend/public/icons/right-arrow.svg rename to frontend/public/icons/rightArrow.svg diff --git a/frontend/src/app/article-viewer/page.tsx b/frontend/src/app/article-viewer/page.tsx new file mode 100644 index 0000000..6e5db27 --- /dev/null +++ b/frontend/src/app/article-viewer/page.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { StaticImport } from "next/dist/shared/lib/get-img-props"; +import Image from "next/image"; +import { useContext } from "react"; + +import backArrow from "@/../public/icons/backArrow.svg"; +import { ArticleContext } from "@/contexts/articleContext"; +import { Article } from "@/hooks/useArticles"; + +const convertDateToMonthDayYear = (date: string): string => { + const textDate = new Date(date).toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", + }); + return textDate; +}; + +/** + * Element to display the selected article in the viewer. + * + * @param param0 + * @returns + */ +const SelectedArticle: React.FC<{ article: Article }> = ({ article }) => { + const formattedDate = convertDateToMonthDayYear(article.dateCreated); + + return ( +
+ {/* Desktop View */} +
+ {article.header} +
+

+ {article.header} +

+

{formattedDate}

+

{article.body}

+
+
+ + {/* Tablet/Mobile View */} +
+

+ {article.header} +

+

{formattedDate}

+ {article.header} +

{article.body}

+
+
+ ); +}; + +const ArticleCard: React.FC<{ article: Article; index: number }> = ({ article, index }) => { + const numCardLimit = 2; + return ( + // Removes the third card in mobile screen +
= numCardLimit ? "hidden md:flex" : ""}`} + > + {article.header} +

{article.header}

+

+ {convertDateToMonthDayYear(article.dateCreated)} +

+

{article.body}

+
+ ); +}; + +const RelatedArticles: React.FC<{ sortedArticles: Article[] }> = ({ sortedArticles }) => { + const hasArticles = sortedArticles.length > 0; + + return ( +
+

+ Related Articles +

+
+ {hasArticles ? ( + sortedArticles + .slice(0, 3) + .map((article, index) => ) + ) : ( +

+ No related articles found. +

+ )} +
+
+ ); +}; + +/** + * + * @param props Takes a Article as a prop to highlight the article in the viewer. + * @returns + */ +const ArticleViewerPage: React.FC<{ selectedArticle: Article }> = ({ selectedArticle }) => { + const { articles, loading } = useContext(ArticleContext); + + // Get sorted articles by recent + const sortedArticles = articles.sort((a, b) => { + const dateA = new Date(a.dateCreated); + const dateB = new Date(b.dateCreated); + return dateA.getTime() - dateB.getTime(); + }); + + return ( +
+
+ + {loading ? ( +

+ Loading article... +

+ ) : ( + + )} +
+
+ {loading ? ( +

+ Loading related articles... +

+ ) : ( + + )} +
+ +
+
+
+ ); +}; +export default ArticleViewerPage; diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index a9316e9..4a0c210 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,8 +1,10 @@ "use client"; import { ThemeProvider } from "@tritonse/tse-constellation"; + import "../global.css"; import { Footer } from "@/components/Footer"; +import { ArticleContextProvider } from "@/contexts/articleContext"; export default function RootLayout({ children, @@ -19,7 +21,7 @@ export default function RootLayout({ primary_dark: "#F05629", }} > - {children} + {children}