Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create article viewer page #55

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion backend/src/models/article.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof articleSchema>;
Expand Down
6 changes: 5 additions & 1 deletion backend/src/validators/article.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Binary file not shown.
4 changes: 4 additions & 0 deletions frontend/public/icons/backArrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
147 changes: 147 additions & 0 deletions frontend/src/app/article-viewer/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
{/* Desktop View */}
<div className="hidden md:flex md:flex-row w-full gap-10">
<img src={article.thumbnail} alt={article.header} className="w-5/12 h-auto object-cover" />

Check warning on line 33 in frontend/src/app/article-viewer/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend lint and style check

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<div className="flex flex-col w-7/12 justify-center gap-5">
<h1 className="mb-2 font-baskerville text-5xl font-bold leading-relaxed">
{article.header}
</h1>
<p className="font-golos text-lg text-orange-500 font-semibold">{formattedDate}</p>
<p className="font-golos text-lg leading-8">{article.body}</p>
</div>
</div>

{/* Tablet/Mobile View */}
<div className="flex md:hidden flex-col w-full gap-7">
<h1 className="font-baskerville text-4xl sm:text-5xl font-bold leading-snug">
{article.header}
</h1>
<p className="font-golos sm:text-lg text-orange-500 font-normal">{formattedDate}</p>
<img src={article.thumbnail} alt={article.header} className="w-full h-auto object-cover" />

Check warning on line 49 in frontend/src/app/article-viewer/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend lint and style check

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
<p className="font-golos sm:text-lg leading-8">{article.body}</p>
</div>
</div>
);
};

const ArticleCard: React.FC<{ article: Article; index: number }> = ({ article, index }) => {
const numCardLimit = 2;
return (
// Removes the third card in mobile screen
<div
className={`flex flex-col w-full h-full gap-2 cursor-pointer font-golos ${index >= numCardLimit ? "hidden md:flex" : ""}`}
>
<img

Check warning on line 63 in frontend/src/app/article-viewer/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend lint and style check

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={article.thumbnail}
alt={article.header}
className={"w-full h-80 mb-3 object-cover"}
/>
<h3 className="mb-2 text-2xl line-clamp-1">{article.header}</h3>
<p className="sm:text-base text-orange-500">
{convertDateToMonthDayYear(article.dateCreated)}
</p>
<p className="sm:text-base line-clamp-4 leading-8">{article.body}</p>
</div>
);
};

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

return (
<div>
<h2 className="mt-5 mb-16 text-orange-500 font-semibold text-4xl md:text-5xl">
Related Articles
</h2>
<div className="flex flex-col md:flex-row gap-10 h-full">
{hasArticles ? (
sortedArticles
.slice(0, 3)
.map((article, index) => <ArticleCard key={index} article={article} index={index} />)
) : (
<p className="flex flow justify-center items-center text-3xl text-gray-400">
No related articles found.
</p>
)}
</div>
</div>
);
};

/**
*
* @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 (
<div className="flex flex-col gap-10 w-full h-full">
<div className="p-10 pb-5">
<button className="flex flex-row items-center gap-2 mb-10 border border-transparent hover:border-b-gray-400">
<Image src={backArrow as StaticImport} alt="back arrow icon" />
<p className="text-lg sm:text-xl text-gray-400">Back to all articles</p>
</button>
{loading ? (
<p className="flex flow justify-center items-center h-96 text-3xl text-gray-400">
Loading article...
</p>
) : (
<SelectedArticle article={selectedArticle} />
)}
</div>
<div className="p-10 bg-gray-100">
{loading ? (
<p className="flex flow justify-center items-center h-96 text-3xl text-gray-400">
Loading related articles...
</p>
) : (
<RelatedArticles sortedArticles={sortedArticles} />
)}
<div className="flex justify-center items-center w-full mt-10 mb-5">
<button className="p-3 border-transparent rounded bg-orange-500 hover:bg-orange-400 text-white font-golos">
See All Articles
</button>
</div>
</div>
</div>
);
};
export default ArticleViewerPage;
4 changes: 3 additions & 1 deletion frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -19,7 +21,7 @@ export default function RootLayout({
primary_dark: "#F05629",
}}
>
{children}
<ArticleContextProvider>{children}</ArticleContextProvider>
<Footer />
</ThemeProvider>
</body>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/EventsCarousel/EventsCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import React, { useEffect, useRef, useState } from "react";

import { EventsCarouselCard } from "./EventsCarouselCard";

import leftArrow from "@/../public/icons/left-arrow.svg";
import rightArrow from "@/../public/icons/right-arrow.svg";
import leftArrow from "@/../public/icons/leftArrow.svg";
import rightArrow from "@/../public/icons/rightArrow.svg";

type EventsCarouselProps = {
children?:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { StaticImport } from "next/dist/shared/lib/get-img-props";
import Image from "next/image";

import learnMoreIcon from "@/../public/icons/learn-more.svg";
import learnMoreIcon from "@/../public/icons/learnMore.svg";

type Event = {
header: string;
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/contexts/articleContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { ReactNode, createContext } from "react";

import { Article, useArticles } from "@/hooks/useArticles";

type ArticleContext = {
articles: Article[];
loading: boolean;
};

// Default context values
export const ArticleContext = createContext<ArticleContext>({
articles: [],
loading: true,
});

export const ArticleContextProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
// Load the articles on mount
const [articles, loading] = useArticles();
return (
<ArticleContext.Provider value={{ articles, loading }}>{children}</ArticleContext.Provider>
);
};
7 changes: 7 additions & 0 deletions frontend/src/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
font-style: normal;
}

@font-face {
font-family: "Baskerville";
src: url("../public/fonts/LibreBaskerville-Regular.ttf") format("truetype");
font-weight: 100 900;
font-style: normal;
}

@layer utilities {
.scrollbar-hidden::-webkit-scrollbar {
display: none;
Expand Down
43 changes: 43 additions & 0 deletions frontend/src/hooks/useArticles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client";

import { useEffect, useState } from "react";

import { get } from "@/api/requests";

export type Article = {
_id: string;
header: string;
dateCreated: string;
author: string;
body?: string | null;
thumbnail?: string;
};

/**
* Fetches all articles from backend on mount
*
* @returns List of articles
* @returns Loading status
*/
export const useArticles = (): [Article[], boolean] => {
const [articles, setArticles] = useState<Article[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
const fetchArticles = async () => {
try {
const response = await get("/articles/all");
const data = (await response.json()) as Article[];
setArticles(data);
} catch (error) {
console.error("Error fetching articles: ", error);
} finally {
setLoading(false);
}
};

void fetchArticles();
}, []);

return [articles, loading];
};
1 change: 1 addition & 0 deletions frontend/tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default {
fontFamily: {
golos: ['"GolosText"', "sans-serif"],
manrope: ['"Manrope"', "sans-serif"],
baskerville: ['"Baskerville"', "sans-serif"],
},
},
},
Expand Down
Loading