From d05012711347e964113eca3ebf5c82e5c8cc5502 Mon Sep 17 00:00:00 2001 From: Roy Dumblauskas Date: Sat, 24 Jan 2026 18:44:37 -0600 Subject: [PATCH] render list of articles from backend --- backend/src/routes/articles.ts | 12 +-- frontend/package-lock.json | 100 +++++++++++++----------- frontend/package.json | 5 +- frontend/src/components/ArticleCard.tsx | 20 +++++ frontend/src/pages/ArticleList.tsx | 41 +++++++++- frontend/src/types/Article.ts | 16 ++++ frontend/tsconfig.json | 13 ++- 7 files changed, 148 insertions(+), 59 deletions(-) create mode 100644 frontend/src/components/ArticleCard.tsx create mode 100644 frontend/src/types/Article.ts diff --git a/backend/src/routes/articles.ts b/backend/src/routes/articles.ts index eb28300..398af63 100644 --- a/backend/src/routes/articles.ts +++ b/backend/src/routes/articles.ts @@ -8,7 +8,7 @@ export const articlesRouter = express.Router(); articlesRouter.get("/getAllArticles", async (_, res: Response) => { const resp = await db.select().from(articles); - res.json({ resp }); + res.json(resp); }); articlesRouter.get("/getArticle/:articleId", async (req: Request, res: Response) => { @@ -17,7 +17,7 @@ articlesRouter.get("/getArticle/:articleId", async (req: Request, res: Response) const resp = await db.select() .from(articles) .where(eq(articles.id, articleId)); - res.json({ resp }); + res.json(resp); }); articlesRouter.get("/getArticleBySlug/:slug", async (req: Request, res: Response) => { @@ -26,7 +26,7 @@ articlesRouter.get("/getArticleBySlug/:slug", async (req: Request, res: Response const resp = await db.select() .from(articles) .where(eq(articles.slug, slug)); - res.json({ resp }); + res.json(resp); }); articlesRouter.post("/postArticle", async (req: Request, res: Response) => { @@ -36,7 +36,7 @@ articlesRouter.post("/postArticle", async (req: Request, res: Response) => { ...req.body, slug: req.body.name.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, ''), }); - res.json({ resp }); + res.json(resp); }); articlesRouter.put("/putArticle/:articleId", async (req: Request, res: Response) => { @@ -52,7 +52,7 @@ articlesRouter.put("/putArticle/:articleId", async (req: Request, res: Response) date_published: req.body.article_status == "published" ? new Date(Date.now()) : null }) .where(eq(articles.id, articleId)); - res.json({ resp }); + res.json(resp); }); articlesRouter.delete("/deleteArticle/:articleId", async (req: Request, res: Response) => { @@ -60,5 +60,5 @@ articlesRouter.delete("/deleteArticle/:articleId", async (req: Request, res: Res const resp = await db.delete(articles) .where(eq(articles.id, articleId)); - res.json({ resp }); + res.json(resp); }); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 16561e6..157dddd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,12 +11,13 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@mui/icons-material": "^7.3.5", - "@mui/material": "^7.3.5", + "@mui/material": "^7.3.7", "@tailwindcss/vite": "^4.1.17", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router-dom": "^7.9.5", - "tailwindcss": "^4.1.17" + "tailwindcss": "^4.1.17", + "zod": "^4.3.6" }, "devDependencies": { "@eslint/js": "^9.36.0", @@ -753,9 +754,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.5.tgz", - "integrity": "sha512-kOLwlcDPnVz2QMhiBv0OQ8le8hTCqKM9cRXlfVPL91l3RGeOsxrIhNRsUt3Xb8wb+pTVUolW+JXKym93vRKxCw==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.7.tgz", + "integrity": "sha512-8jWwS6FweMkpyRkrJooamUGe1CQfO1yJ+lM43IyUJbrhHW/ObES+6ry4vfGi8EKaldHL3t3BG1bcLcERuJPcjg==", "license": "MIT", "funding": { "type": "opencollective", @@ -789,22 +790,22 @@ } }, "node_modules/@mui/material": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz", - "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.7.tgz", + "integrity": "sha512-6bdIxqzeOtBAj2wAsfhWCYyMKPLkRO9u/2o5yexcL0C3APqyy91iGSWgT3H7hg+zR2XgE61+WAu12wXPON8b6A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", - "@mui/core-downloads-tracker": "^7.3.5", - "@mui/system": "^7.3.5", - "@mui/types": "^7.4.8", - "@mui/utils": "^7.3.5", + "@mui/core-downloads-tracker": "^7.3.7", + "@mui/system": "^7.3.7", + "@mui/types": "^7.4.10", + "@mui/utils": "^7.3.7", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", - "csstype": "^3.1.3", + "csstype": "^3.2.3", "prop-types": "^15.8.1", - "react-is": "^19.2.0", + "react-is": "^19.2.3", "react-transition-group": "^4.4.5" }, "engines": { @@ -817,7 +818,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^7.3.5", + "@mui/material-pigment-css": "^7.3.7", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -838,13 +839,13 @@ } }, "node_modules/@mui/private-theming": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.5.tgz", - "integrity": "sha512-cTx584W2qrLonwhZLbEN7P5pAUu0nZblg8cLBlTrZQ4sIiw8Fbvg7GvuphQaSHxPxrCpa7FDwJKtXdbl2TSmrA==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.7.tgz", + "integrity": "sha512-w7r1+CYhG0syCAQUWAuV5zSaU2/67WA9JXUderdb7DzCIJdp/5RmJv6L85wRjgKCMsxFF0Kfn0kPgPbPgw/jdw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", - "@mui/utils": "^7.3.5", + "@mui/utils": "^7.3.7", "prop-types": "^15.8.1" }, "engines": { @@ -865,16 +866,16 @@ } }, "node_modules/@mui/styled-engine": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.5.tgz", - "integrity": "sha512-zbsZ0uYYPndFCCPp2+V3RLcAN6+fv4C8pdwRx6OS3BwDkRCN8WBehqks7hWyF3vj1kdQLIWrpdv/5Y0jHRxYXQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.7.tgz", + "integrity": "sha512-y/QkNXv6cF6dZ5APztd/dFWfQ6LHKPx3skyYO38YhQD4+Cxd6sFAL3Z38WMSSC8LQz145Mpp3CcLrSCLKPwYAg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/sheet": "^1.4.0", - "csstype": "^3.1.3", + "csstype": "^3.2.3", "prop-types": "^15.8.1" }, "engines": { @@ -899,18 +900,18 @@ } }, "node_modules/@mui/system": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.5.tgz", - "integrity": "sha512-yPaf5+gY3v80HNkJcPi6WT+r9ebeM4eJzrREXPxMt7pNTV/1eahyODO4fbH3Qvd8irNxDFYn5RQ3idHW55rA6g==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.7.tgz", + "integrity": "sha512-DovL3k+FBRKnhmatzUMyO5bKkhMLlQ9L7Qw5qHrre3m8zCZmE+31NDVBFfqrbrA7sq681qaEIHdkWD5nmiAjyQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", - "@mui/private-theming": "^7.3.5", - "@mui/styled-engine": "^7.3.5", - "@mui/types": "^7.4.8", - "@mui/utils": "^7.3.5", + "@mui/private-theming": "^7.3.7", + "@mui/styled-engine": "^7.3.7", + "@mui/types": "^7.4.10", + "@mui/utils": "^7.3.7", "clsx": "^2.1.1", - "csstype": "^3.1.3", + "csstype": "^3.2.3", "prop-types": "^15.8.1" }, "engines": { @@ -939,9 +940,9 @@ } }, "node_modules/@mui/types": { - "version": "7.4.8", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.8.tgz", - "integrity": "sha512-ZNXLBjkPV6ftLCmmRCafak3XmSn8YV0tKE/ZOhzKys7TZXUiE0mZxlH8zKDo6j6TTUaDnuij68gIG+0Ucm7Xhw==", + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.10.tgz", + "integrity": "sha512-0+4mSjknSu218GW3isRqoxKRTOrTLd/vHi/7UC4+wZcUrOAqD9kRk7UQRL1mcrzqRoe7s3UT6rsRpbLkW5mHpQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4" @@ -956,17 +957,17 @@ } }, "node_modules/@mui/utils": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.5.tgz", - "integrity": "sha512-jisvFsEC3sgjUjcPnR4mYfhzjCDIudttSGSbe1o/IXFNu0kZuR+7vqQI0jg8qtcVZBHWrwTfvAZj9MNMumcq1g==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.7.tgz", + "integrity": "sha512-+YjnjMRnyeTkWnspzoxRdiSOgkrcpTikhNPoxOZW0APXx+urHtUoXJ9lbtCZRCA5a4dg5gSbd19alL1DvRs5fg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", - "@mui/types": "^7.4.8", + "@mui/types": "^7.4.10", "@types/prop-types": "^15.7.15", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^19.2.0" + "react-is": "^19.2.3" }, "engines": { "node": ">=14.0.0" @@ -2274,9 +2275,9 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/debug": { @@ -3626,9 +3627,9 @@ } }, "node_modules/react-is": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", - "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", + "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", "license": "MIT" }, "node_modules/react-refresh": { @@ -4265,6 +4266,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/frontend/package.json b/frontend/package.json index b54d02e..3c3a83e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,12 +13,13 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@mui/icons-material": "^7.3.5", - "@mui/material": "^7.3.5", + "@mui/material": "^7.3.7", "@tailwindcss/vite": "^4.1.17", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router-dom": "^7.9.5", - "tailwindcss": "^4.1.17" + "tailwindcss": "^4.1.17", + "zod": "^4.3.6" }, "devDependencies": { "@eslint/js": "^9.36.0", diff --git a/frontend/src/components/ArticleCard.tsx b/frontend/src/components/ArticleCard.tsx new file mode 100644 index 0000000..a2985d7 --- /dev/null +++ b/frontend/src/components/ArticleCard.tsx @@ -0,0 +1,20 @@ +import { useNavigate } from 'react-router-dom'; +import type { Article } from '../types/Article.ts'; +import { Card } from '@mui/material'; + +interface Props { + article: Article; +} + +export default function ArticleCard(props: Props) { + const navigate = useNavigate(); + + return ( + { navigate(`/article/${props.article.slug}`) }} + > + {props.article.name} + + ); +}; diff --git a/frontend/src/pages/ArticleList.tsx b/frontend/src/pages/ArticleList.tsx index 5830ced..f67ac9e 100644 --- a/frontend/src/pages/ArticleList.tsx +++ b/frontend/src/pages/ArticleList.tsx @@ -1,7 +1,42 @@ +import { useState } from 'react'; +import { Button } from '@mui/material'; +import type { Article } from '../types/Article.ts'; +import { ArticleListSchema } from '../types/Article.ts'; +import ArticleCard from '../components/ArticleCard.tsx'; + export default function ArticleList() { + const [articles, setArticles] = useState([]); + + async function getAllArticles() { + const result = await fetch( + "/api/articles/getAllArticles", + { + method: "GET", + headers: { "Accept": "application/json" } + } + ); + + const json = await result.json(); + const data = ArticleListSchema.parse(json); + console.log(data); + setArticles(data); + + }; + return ( -
- List of articles to be populated and searchable -
+
+

+ List of articles to be populated and searchable +

+ +
+ {articles.map((article) => ())} +
+
); }; diff --git a/frontend/src/types/Article.ts b/frontend/src/types/Article.ts new file mode 100644 index 0000000..925247a --- /dev/null +++ b/frontend/src/types/Article.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +export const ArticleSchema = z.object({ + id: z.string(), + name: z.string(), + article_status: z.enum(["draft", "published", "archived"]), + author_id: z.string(), + slug: z.string(), + summary: z.string(), + date_created: z.string(), + last_edit: z.string(), + date_published: z.string().nullable(), +}); + +export const ArticleListSchema = z.array(ArticleSchema); +export type Article = z.infer; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 1ffef60..cd68bb2 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,7 +1,14 @@ { "files": [], "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ], + "compilerOptions": { + "strict": true + } }