From 29f359610f1a2ad0330c560bce7a73a2c42c3387 Mon Sep 17 00:00:00 2001 From: Janis Date: Tue, 7 Feb 2023 15:42:40 +0100 Subject: [PATCH] refactor --- components/AdminNav.tsx | 1 - components/ArticleControl.tsx | 31 ++ components/CategoryControl.tsx | 31 ++ components/ContentTable.tsx | 46 ++- components/Nav.tsx | 275 ++++++++++-------- pages/_app.tsx | 4 +- pages/_document.tsx | 3 +- pages/admin/editor/article/[articleId].tsx | 2 +- pages/api/articles/[articleId].ts | 19 +- pages/api/categories/[categoryId].ts | 18 +- pages/api/categories/index.ts | 125 ++++---- pages/api/search.ts | 30 ++ .../[categoryName]/[articleName]/index.tsx | 43 +-- pages/articles/[categoryName]/index.tsx | 50 ++-- .../20230207142155_ondelete/migration.sql | 5 + prisma/schema.prisma | 2 +- 16 files changed, 421 insertions(+), 264 deletions(-) create mode 100644 components/ArticleControl.tsx create mode 100644 components/CategoryControl.tsx create mode 100644 pages/api/search.ts create mode 100644 prisma/migrations/20230207142155_ondelete/migration.sql diff --git a/components/AdminNav.tsx b/components/AdminNav.tsx index b15dbde..f7700da 100644 --- a/components/AdminNav.tsx +++ b/components/AdminNav.tsx @@ -5,7 +5,6 @@ import styles from "@/styles/modules/AdminNav.module.scss"; function AdminNav() { return (
- Admin New article New category
diff --git a/components/ArticleControl.tsx b/components/ArticleControl.tsx new file mode 100644 index 0000000..efd99d1 --- /dev/null +++ b/components/ArticleControl.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { apiUrl } from "@/global"; +import urlJoin from "url-join"; +import { useRouter } from "next/navigation"; + +export default function ArticleControl({ articleId }: { articleId: string }) { + const router = useRouter(); + async function deleteArticle() { + await fetch(urlJoin(apiUrl, `articles/${articleId}`), { method: "DELETE" }) + .then((response) => response.json()) + .then((result) => { + console.log(result); + }); + router.push("/articles"); + } + + function editArticle() { + router.push("/admin/editor/article/" + articleId); + } + + return ( +
+ + +
+ ); +} diff --git a/components/CategoryControl.tsx b/components/CategoryControl.tsx new file mode 100644 index 0000000..99fe66a --- /dev/null +++ b/components/CategoryControl.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { apiUrl } from "@/global"; +import urlJoin from "url-join"; +import { useRouter } from "next/navigation"; + +export default function CategoryControl({ categoryId }: { categoryId: string }) { + const router = useRouter(); + async function deleteCategory() { + await fetch(urlJoin(apiUrl, `categories/${categoryId}`), { method: "DELETE" }) + .then((response) => response.json()) + .then((result) => { + console.log(result); + }); + router.push("/articles"); + } + + function editCategory() { + router.push("/admin/editor/category/" + categoryId); + } + + return ( +
+ + +
+ ); +} diff --git a/components/ContentTable.tsx b/components/ContentTable.tsx index a3e1752..b6a186a 100644 --- a/components/ContentTable.tsx +++ b/components/ContentTable.tsx @@ -1,31 +1,25 @@ import React from "react"; import styles from "@/styles/modules/ArticleContentTable.module.scss"; import { IContentTableEntry } from "../types/contentTable"; +import { CLIENT_RENEG_LIMIT } from "tls"; -export default function ContentTable({ - contentTableData, -}: { - contentTableData: any; -}) { - return ( -
-
-
-

Contents

- {contentTableData?.map((e: IContentTableEntry, i: number) => { - return ( - - {e.title} - - ); - })} -
- {contentTableData?.length < 15 ? ( -
Future advertisement
- ) : ( - "" - )} -
-
- ); +export default function ContentTable({ contentTableData }: { contentTableData: IContentTableEntry[] }) { + console.log(contentTableData); + return ( +
+
+
+

Contents

+ {contentTableData.map((e: IContentTableEntry, i: number) => { + return ( + + {e.title} + + ); + })} +
+ {contentTableData?.length < 15 ?
Future advertisement
: ""} +
+
+ ); } diff --git a/components/Nav.tsx b/components/Nav.tsx index 497107a..37d5eaa 100644 --- a/components/Nav.tsx +++ b/components/Nav.tsx @@ -3,147 +3,168 @@ import Image from "next/image"; import Link from "next/link"; import { useEffect, useState } from "react"; import { Category } from "@prisma/client"; +import prisma, { CategoryWithIncludes } from "@/lib/prisma"; +import { CLIENT_RENEG_LIMIT } from "tls"; +import { apiUrl } from "@/global"; +import urlJoin from "url-join"; function switchTheme(theme: string) { - const bodyElement = document.getElementsByTagName("body")[0]; + const bodyElement = document.getElementsByTagName("body")[0]; - if (theme == "dark") { - bodyElement.classList.remove("theme-light"); - } else { - bodyElement.classList.add("theme-light"); - } + if (theme == "dark") { + bodyElement.classList.remove("theme-light"); + } else { + bodyElement.classList.add("theme-light"); + } } function toggleTheme() { - const svgElement = document.getElementById("themeSwitchSvg"); + const svgElement = document.getElementById("themeSwitchSvg"); - if (svgElement) { - if (localStorage.getItem("theme") == "light") { - svgElement.style.animationDirection = "normal"; - svgElement.style.animationName = styles.spinThemeSwitch; - } else { - svgElement.style.animationDirection = "reverse"; - svgElement.style.animationName = styles.spinThemeSwitch; - } + if (svgElement) { + if (localStorage.getItem("theme") == "light") { + svgElement.style.animationDirection = "normal"; + svgElement.style.animationName = styles.spinThemeSwitch; + } else { + svgElement.style.animationDirection = "reverse"; + svgElement.style.animationName = styles.spinThemeSwitch; + } - setTimeout(() => { - if (localStorage.getItem("theme") == "light") { - localStorage.setItem("theme", "dark"); - switchTheme("dark"); - } else { - localStorage.setItem("theme", "light"); - switchTheme("light"); - } - svgElement.style.animationName = ""; - }, 150); - } + setTimeout(() => { + if (localStorage.getItem("theme") == "light") { + localStorage.setItem("theme", "dark"); + switchTheme("dark"); + } else { + localStorage.setItem("theme", "light"); + switchTheme("light"); + } + svgElement.style.animationName = ""; + }, 150); + } } -export default function Nav({ categories }: { categories: Category[] }) { - const [searchResults, setSearchResults] = useState< - { name: string; title: string }[] - >([]); +export default function Nav() { + const [searchResults, setSearchResults] = useState<{ name: string; title: string }[]>([]); + const [categories, setCategories] = useState([]); - async function handleSearchInput(event: React.ChangeEvent) { - const query = event.target.value; - let result = await fetch(`/api/search?q=${query}`); - let json = await result.json(); + async function handleSearchInput(event: React.ChangeEvent) { + const query = event.target.value; + let result = await fetch(`/api/search?q=${query}`); + let json = await result.json(); - if (json.length == 0 && query.length > 0) { - setSearchResults([{ name: "", title: "No article found..." }]); - } else { - setSearchResults(json); - } - } + if (json.length == 0 && query.length > 0) { + setSearchResults([{ name: "", title: "No article found..." }]); + } else { + setSearchResults(json); + } + } - useEffect(() => { - if (localStorage.getItem("theme") == "dark") { - switchTheme("dark"); - } else { - switchTheme("light"); - } - }, []); + useEffect(() => { + if (localStorage.getItem("theme") == "dark") { + switchTheme("dark"); + } else { + switchTheme("light"); + } + }, []); - useEffect(() => { - console.log(searchResults); - }, [searchResults]); + useEffect(() => { + console.log(searchResults); + }, [searchResults]); - return ( - - ); + getCategories(); + }, []); + return ( + + ); +} + +export async function getServerSideProps() { + let categories: Category[] = []; + await prisma.category.findMany({ include: { articles: true, svg: true } }).then( + (result: Category[]) => { + if (result) { + categories = JSON.parse(JSON.stringify(result)); + } + }, + (reason: any) => { + console.log(reason); + } + ); + + return { + props: { categories: categories }, // will be passed to the page component as props + }; } diff --git a/pages/_app.tsx b/pages/_app.tsx index 7d7044c..669de45 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -6,12 +6,14 @@ import type { AppProps } from "next/app"; import AdminNav from "@/components/AdminNav"; import Footer from "@/components/Footer"; import Nav from "@/components/Nav"; +import { Category } from "@prisma/client"; +import prisma from "@/lib/prisma"; export default function App({ Component, pageProps }: AppProps) { return ( <>
-
diff --git a/pages/_document.tsx b/pages/_document.tsx index 653b4dd..ce02aa2 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -2,11 +2,10 @@ import { Html, Head, Main, NextScript } from "next/document"; export default function Document() { return ( - +
- diff --git a/pages/admin/editor/article/[articleId].tsx b/pages/admin/editor/article/[articleId].tsx index 037bcc8..d6d207c 100644 --- a/pages/admin/editor/article/[articleId].tsx +++ b/pages/admin/editor/article/[articleId].tsx @@ -44,7 +44,7 @@ export default function AdminArticlesEditorPage({ article, categories }: { artic function changeContentTableEntryTitle(index: number, newTitle: string) { setContentTable((prevArray: any) => { let newArray = [...prevArray]; - newArray[index].anchor = newTitle; + newArray[index].title = newTitle; return newArray; }); } diff --git a/pages/api/articles/[articleId].ts b/pages/api/articles/[articleId].ts index 7d92437..4db8bb6 100644 --- a/pages/api/articles/[articleId].ts +++ b/pages/api/articles/[articleId].ts @@ -1,19 +1,19 @@ import { Prisma, Article } from "@prisma/client"; import { ResponseError } from "../../../types/responseErrors"; import { formatTextToUrlName } from "../../../utils"; -import prisma from "../../../lib/prisma"; +import prisma, { ArticleWithIncludes } from "../../../lib/prisma"; import type { NextApiRequest, NextApiResponse } from 'next' import { UpdateArticle } from "../../../types/api"; import { isValidText } from "../../../validators"; -type ArticleWithIncludes = Prisma.ArticleGetPayload<{ include: { contentTableEntries: true, category: true, image: true } }> export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const articleId: string = formatTextToUrlName(req.query?.articleId?.toString() ?? "") if (req.method == "PUT") {//* PUT console.log("PUT") - const articleId: string = formatTextToUrlName(req.query?.articleId?.toString() ?? "") + console.log(`API articleId: ${articleId}`) const articleData: UpdateArticle = req.body; @@ -55,6 +55,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) console.error(err); res.status(500).end(); }); + } else if (req.method == "DELETE") { + console.log("DELETE article") + prisma.article.delete({ where: { id: articleId }, include: { category: true } }).then((result: ArticleWithIncludes | null) => { + if (result) { + res.status(200).json(result) + } else { + res.status(500).json({ error: true, message: "No article found" }) + } + }, (err) => { + console.log(err) + res.status(500).end(err) + }) } - } diff --git a/pages/api/categories/[categoryId].ts b/pages/api/categories/[categoryId].ts index f291dcc..d4d47d5 100644 --- a/pages/api/categories/[categoryId].ts +++ b/pages/api/categories/[categoryId].ts @@ -1,4 +1,4 @@ -import prisma from "../../../lib/prisma"; +import prisma, { CategoryWithIncludes } from "../../../lib/prisma"; import { Category } from "@prisma/client"; import { ResponseError } from "../../../types/responseErrors"; import { Prisma } from "@prisma/client"; @@ -9,10 +9,10 @@ import { isValidText } from "../../../validators"; import { UpdateCategory } from '../../../types/api'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - + const categoryId: string = req.query.categoryId?.toString() ?? ""; if (req.method == "PUT") { - const categoryId: string = req.query.categoryId?.toString() ?? ""; + const categoryData: UpdateCategory = req.body; if (categoryData.title && !isValidText(categoryData.title)) { @@ -66,5 +66,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) console.error(err); res.status(500).end(); }); + } else if (req.method == "DELETE") { + console.log("DELETE category") + prisma.category.delete({ where: { id: categoryId }, include: { articles: true, svg: true } }).then((result: CategoryWithIncludes | null) => { + if (result) { + res.status(200).json(result) + } else { + res.status(500).json({ error: true, message: "No category found" }) + } + }, (err) => { + console.log(err) + res.status(500).end(err) + }) } } diff --git a/pages/api/categories/index.ts b/pages/api/categories/index.ts index 484b5e0..c0d2db2 100644 --- a/pages/api/categories/index.ts +++ b/pages/api/categories/index.ts @@ -10,66 +10,77 @@ import type { NextApiRequest, NextApiResponse } from 'next' import { CreateCategory } from "@/types/api"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (req.method == "POST") { - console.log("API new category") - const categoryData: CreateCategory = req.body; - console.log(categoryData) + if (req.method == "GET") { + console.log("API get categories") + await prisma.category.findMany().then((result: Category[]) => { + if (result) { + res.status(200).json(result) + } + }, (errorReason) => { + console.log(errorReason); + res.status(500).end(errorReason); + }) + } else + if (req.method == "POST") { + console.log("API new category") + const categoryData: CreateCategory = req.body; + console.log(categoryData) - if (!isValidText(categoryData.title)) { - res.json({ target: "title", error: "Not a valid title" }); - return; - } + if (!isValidText(categoryData.title)) { + res.json({ target: "title", error: "Not a valid title" }); + return; + } - categoryData.svg.viewbox = categoryData.svg.viewbox.length > 1 ? categoryData.svg.viewbox : ""; + categoryData.svg.viewbox = categoryData.svg.viewbox.length > 1 ? categoryData.svg.viewbox : ""; - const newSvg: Prisma.SvgUncheckedCreateInput = { - viewbox: categoryData.svg.viewbox, - path: categoryData.svg.path - } + const newSvg: Prisma.SvgUncheckedCreateInput = { + viewbox: categoryData.svg.viewbox, + path: categoryData.svg.path + } - await prisma.svg - .create({ data: newSvg }) - .then( - async (createdSvg: Svg) => { - const newCategory: Prisma.CategoryUncheckedCreateInput = { - title: categoryData.title, - name: formatTextToUrlName(categoryData.title), - color: categoryData.color ?? "teal", - svgId: createdSvg.id, + await prisma.svg + .create({ data: newSvg }) + .then( + async (createdSvg: Svg) => { + const newCategory: Prisma.CategoryUncheckedCreateInput = { + title: categoryData.title, + name: formatTextToUrlName(categoryData.title), + color: categoryData.color ?? "teal", + svgId: createdSvg.id, + } + + await prisma.category + .create({ + data: newCategory, + include: { svg: true, articles: true }, + }) + .then( + (createdCategory: CategoryWithIncludes | null) => { + if (createdCategory) { + res.json({ success: true, data: createdCategory }); + } else { + res.json({ error: true, message: "Could not create category" }); + } + }, + (errorReason) => { + console.log(errorReason) + if (errorReason.code === "P2002") { + res.json({ target: errorReason.meta.target[0], error: "Already exists" }); + } + } + ) + .catch((err) => { + console.error(err); + res.status(500).end(); + }); + }, + (errorReason) => { + res.status(500).end(errorReason); } - - await prisma.category - .create({ - data: newCategory, - include: { svg: true, articles: true }, - }) - .then( - (createdCategory: CategoryWithIncludes | null) => { - if (createdCategory) { - res.json({ success: true, data: createdCategory }); - } else { - res.json({ error: true, message: "Could not create category" }); - } - }, - (errorReason) => { - console.log(errorReason) - if (errorReason.code === "P2002") { - res.json({ target: errorReason.meta.target[0], error: "Already exists" }); - } - } - ) - .catch((err) => { - console.error(err); - res.status(500).end(); - }); - }, - (errorReason) => { - res.status(500).end(errorReason); - } - ) - .catch((err) => { - console.error(err); - res.status(500).end(); - }); - } + ) + .catch((err) => { + console.error(err); + res.status(500).end(); + }); + } } diff --git a/pages/api/search.ts b/pages/api/search.ts new file mode 100644 index 0000000..6179b4a --- /dev/null +++ b/pages/api/search.ts @@ -0,0 +1,30 @@ +import prisma from "../../lib/prisma"; +import type { NextApiRequest, NextApiResponse } from 'next' +import { formatTextToUrlName } from '../../utils'; +import { Prisma } from '@prisma/client'; + +type SearchArticle = Prisma.ArticleGetPayload<{ select: { title: true, name: true } }> + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + let query: string = req.query.q?.toString() ?? ""; + query = formatTextToUrlName(query) + if (query.length > 0) { + await prisma.article.findMany({ + select: { title: true, name: true }, + take: 5, + }).then((result: SearchArticle[]) => { + let searchResult: SearchArticle[] = [] + result.forEach((a: SearchArticle) => { + if (a.name.includes(query)) { + searchResult.push(a); + } + }); + res.status(200).json(searchResult); + }, (err: any) => { + console.log(err) + res.status(200).json([]); + }); + } else { + res.status(200).json([]); + } +} \ No newline at end of file diff --git a/pages/articles/[categoryName]/[articleName]/index.tsx b/pages/articles/[categoryName]/[articleName]/index.tsx index f9287f3..5b2fdc2 100644 --- a/pages/articles/[categoryName]/[articleName]/index.tsx +++ b/pages/articles/[categoryName]/[articleName]/index.tsx @@ -5,7 +5,9 @@ import Image from "next/image"; import Markdown from "@/components/Markdown"; import { formatTextToUrlName } from "@/utils"; import prisma, { ArticleWithIncludes, CategoryWithIncludes } from "@/lib/prisma"; -import articles from "../.."; +import ArticleControl from "../../../../components/ArticleControl"; +import { IContentTableEntry } from "@/types/contentTable"; +import { Prisma } from "@prisma/client"; //* MAIN export default function ArticlePage({ article }: { article: ArticleWithIncludes }) { @@ -14,27 +16,32 @@ export default function ArticlePage({ article }: { article: ArticleWithIncludes const dateOptions: Intl.DateTimeFormatOptions = { month: "long", day: "numeric", year: "numeric" }; return ( -
- -
-
-

- {`Published on ${dateCreated.toLocaleDateString("en-US", dateOptions)}`} -
- {dateUpdated > dateCreated ? `Updated on ${dateUpdated.toLocaleDateString("en-US", dateOptions)}` : ""} -

+ <> + +
+ ({ anchor: c.anchor, title: c.title })) : []} + /> +
+
+

+ {`Published on ${dateCreated.toLocaleDateString("en-US", dateOptions)}`} +
+ {dateUpdated > dateCreated ? `Updated on ${dateUpdated.toLocaleDateString("en-US", dateOptions)}` : ""} +

-

{article?.title}

-
- Docker Setup Ubuntu +

{article?.title}

+ + {""} +

{article?.introduction}

- {""} -

{article?.introduction}

+
- +
- -
+ ); } diff --git a/pages/articles/[categoryName]/index.tsx b/pages/articles/[categoryName]/index.tsx index 3cf5eca..d755178 100644 --- a/pages/articles/[categoryName]/index.tsx +++ b/pages/articles/[categoryName]/index.tsx @@ -3,15 +3,18 @@ import Link from "next/link"; import { formatTextToUrlName } from "@/utils"; import { Article, Category } from "@prisma/client"; import prisma, { CategoryWithIncludes } from "@/lib/prisma"; +import CategoryControl from "../../../components/CategoryControl"; -export default function CategoryPage({ category }: { category: CategoryWithIncludes | null }) { +export default function CategoryPage({ category }: { category: CategoryWithIncludes }) { return ( -
-

{category?.title}

-
-
-

Most popular articles

- {/* {popularArticles?.map((a, i) => { + <> + +
+

{category?.title}

+
+
+

Most popular articles

+ {/* {popularArticles?.map((a, i) => { { return ( @@ -20,9 +23,9 @@ export default function CategoryPage({ category }: { category: CategoryWithInclu ); } })} */} -
+
- {/*
+ {/*

Most recent articles

{recentArticles?.map((a, i) => { { @@ -35,22 +38,23 @@ export default function CategoryPage({ category }: { category: CategoryWithInclu })}
*/} -
-

All articles

- {category?.articles - ? Array.from(category?.articles).map((a: Article, i: number) => { - { - return ( - - {a.title} - - ); - } - }) - : ""} +
+

All articles

+ {category?.articles + ? Array.from(category?.articles).map((a: Article, i: number) => { + { + return ( + + {a.title} + + ); + } + }) + : ""} +
-
+ ); } export async function getServerSideProps(context: any) { diff --git a/prisma/migrations/20230207142155_ondelete/migration.sql b/prisma/migrations/20230207142155_ondelete/migration.sql new file mode 100644 index 0000000..2c3c7ac --- /dev/null +++ b/prisma/migrations/20230207142155_ondelete/migration.sql @@ -0,0 +1,5 @@ +-- DropForeignKey +ALTER TABLE "Article" DROP CONSTRAINT "Article_categoryId_fkey"; + +-- AddForeignKey +ALTER TABLE "Article" ADD CONSTRAINT "Article_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7ff6ce0..6b88e97 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -16,7 +16,7 @@ model Article { markdown String contentTable Json? categoryId String - category Category @relation(fields: [categoryId], references: [id]) + category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade) dateCreated DateTime @default(now()) dateUpdated DateTime @default(now()) }