render list of articles from backend

This commit is contained in:
2026-01-24 18:44:37 -06:00
parent c5e8faf9ed
commit d050127113
7 changed files with 148 additions and 59 deletions
+6 -6
View File
@@ -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);
});
+55 -45
View File
@@ -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"
}
}
}
}
+3 -2
View File
@@ -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",
+20
View File
@@ -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 (
<Card
className="p-1 cursor-pointer"
onClick={() => { navigate(`/article/${props.article.slug}`) }}
>
{props.article.name}
</Card>
);
};
+38 -3
View File
@@ -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<Article[]>([]);
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 (
<div>
List of articles to be populated and searchable
</div>
<div className="p-3 flex flex-col gap-5">
<p>
List of articles to be populated and searchable
</p>
<Button
style={{ border: "solid", borderWidth: "1px" }}
onClick={() => getAllArticles()}
>
Get All Articles
</Button>
<div>
{articles.map((article) => (<ArticleCard key={article.id} article={article} />))}
</div>
</div >
);
};
+16
View File
@@ -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<typeof ArticleSchema>;
+10 -3
View File
@@ -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
}
}