mirror of
https://github.com/DerTyp7/explainegy-nextjs.git
synced 2025-10-29 21:02:13 +01:00
refactor
This commit is contained in:
@@ -5,7 +5,6 @@ import styles from "@/styles/modules/AdminNav.module.scss";
|
|||||||
function AdminNav() {
|
function AdminNav() {
|
||||||
return (
|
return (
|
||||||
<div className={styles.adminNav}>
|
<div className={styles.adminNav}>
|
||||||
<Link href={"/admin"}>Admin</Link>
|
|
||||||
<Link href={"/admin/editor/article/0"}>New article</Link>
|
<Link href={"/admin/editor/article/0"}>New article</Link>
|
||||||
<Link href={"/admin/editor/category/0"}>New category</Link>
|
<Link href={"/admin/editor/category/0"}>New category</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
31
components/ArticleControl.tsx
Normal file
31
components/ArticleControl.tsx
Normal file
@@ -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 (
|
||||||
|
<div>
|
||||||
|
<button className="danger" onClick={deleteArticle}>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
<button className="warning" onClick={editArticle}>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
components/CategoryControl.tsx
Normal file
31
components/CategoryControl.tsx
Normal file
@@ -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 (
|
||||||
|
<div>
|
||||||
|
<button className="danger" onClick={deleteCategory}>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
<button className="warning" onClick={editCategory}>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,31 +1,25 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import styles from "@/styles/modules/ArticleContentTable.module.scss";
|
import styles from "@/styles/modules/ArticleContentTable.module.scss";
|
||||||
import { IContentTableEntry } from "../types/contentTable";
|
import { IContentTableEntry } from "../types/contentTable";
|
||||||
|
import { CLIENT_RENEG_LIMIT } from "tls";
|
||||||
|
|
||||||
export default function ContentTable({
|
export default function ContentTable({ contentTableData }: { contentTableData: IContentTableEntry[] }) {
|
||||||
contentTableData,
|
console.log(contentTableData);
|
||||||
}: {
|
return (
|
||||||
contentTableData: any;
|
<div className={styles.articleContentTable}>
|
||||||
}) {
|
<div className={styles.stickyContainer}>
|
||||||
return (
|
<div className={styles.list}>
|
||||||
<div className={styles.articleContentTable}>
|
<h2>Contents</h2>
|
||||||
<div className={styles.stickyContainer}>
|
{contentTableData.map((e: IContentTableEntry, i: number) => {
|
||||||
<div className={styles.list}>
|
return (
|
||||||
<h2>Contents</h2>
|
<a key={i} href={"#" + e.anchor}>
|
||||||
{contentTableData?.map((e: IContentTableEntry, i: number) => {
|
{e.title}
|
||||||
return (
|
</a>
|
||||||
<a key={i} href={"#" + e.anchor}>
|
);
|
||||||
{e.title}
|
})}
|
||||||
</a>
|
</div>
|
||||||
);
|
{contentTableData?.length < 15 ? <div className={styles.adContainer}>Future advertisement</div> : ""}
|
||||||
})}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{contentTableData?.length < 15 ? (
|
);
|
||||||
<div className={styles.adContainer}>Future advertisement</div>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,147 +3,168 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Category } from "@prisma/client";
|
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) {
|
function switchTheme(theme: string) {
|
||||||
const bodyElement = document.getElementsByTagName("body")[0];
|
const bodyElement = document.getElementsByTagName("body")[0];
|
||||||
|
|
||||||
if (theme == "dark") {
|
if (theme == "dark") {
|
||||||
bodyElement.classList.remove("theme-light");
|
bodyElement.classList.remove("theme-light");
|
||||||
} else {
|
} else {
|
||||||
bodyElement.classList.add("theme-light");
|
bodyElement.classList.add("theme-light");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
const svgElement = document.getElementById("themeSwitchSvg");
|
const svgElement = document.getElementById("themeSwitchSvg");
|
||||||
|
|
||||||
if (svgElement) {
|
if (svgElement) {
|
||||||
if (localStorage.getItem("theme") == "light") {
|
if (localStorage.getItem("theme") == "light") {
|
||||||
svgElement.style.animationDirection = "normal";
|
svgElement.style.animationDirection = "normal";
|
||||||
svgElement.style.animationName = styles.spinThemeSwitch;
|
svgElement.style.animationName = styles.spinThemeSwitch;
|
||||||
} else {
|
} else {
|
||||||
svgElement.style.animationDirection = "reverse";
|
svgElement.style.animationDirection = "reverse";
|
||||||
svgElement.style.animationName = styles.spinThemeSwitch;
|
svgElement.style.animationName = styles.spinThemeSwitch;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (localStorage.getItem("theme") == "light") {
|
if (localStorage.getItem("theme") == "light") {
|
||||||
localStorage.setItem("theme", "dark");
|
localStorage.setItem("theme", "dark");
|
||||||
switchTheme("dark");
|
switchTheme("dark");
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem("theme", "light");
|
localStorage.setItem("theme", "light");
|
||||||
switchTheme("light");
|
switchTheme("light");
|
||||||
}
|
}
|
||||||
svgElement.style.animationName = "";
|
svgElement.style.animationName = "";
|
||||||
}, 150);
|
}, 150);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Nav({ categories }: { categories: Category[] }) {
|
export default function Nav() {
|
||||||
const [searchResults, setSearchResults] = useState<
|
const [searchResults, setSearchResults] = useState<{ name: string; title: string }[]>([]);
|
||||||
{ name: string; title: string }[]
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
>([]);
|
|
||||||
|
|
||||||
async function handleSearchInput(event: React.ChangeEvent<HTMLInputElement>) {
|
async function handleSearchInput(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
const query = event.target.value;
|
const query = event.target.value;
|
||||||
let result = await fetch(`/api/search?q=${query}`);
|
let result = await fetch(`/api/search?q=${query}`);
|
||||||
let json = await result.json();
|
let json = await result.json();
|
||||||
|
|
||||||
if (json.length == 0 && query.length > 0) {
|
if (json.length == 0 && query.length > 0) {
|
||||||
setSearchResults([{ name: "", title: "No article found..." }]);
|
setSearchResults([{ name: "", title: "No article found..." }]);
|
||||||
} else {
|
} else {
|
||||||
setSearchResults(json);
|
setSearchResults(json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (localStorage.getItem("theme") == "dark") {
|
if (localStorage.getItem("theme") == "dark") {
|
||||||
switchTheme("dark");
|
switchTheme("dark");
|
||||||
} else {
|
} else {
|
||||||
switchTheme("light");
|
switchTheme("light");
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(searchResults);
|
console.log(searchResults);
|
||||||
}, [searchResults]);
|
}, [searchResults]);
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<nav className={styles.nav}>
|
async function getCategories() {
|
||||||
<div className={styles.containerLeft}>
|
await fetch(urlJoin(apiUrl, "categories"))
|
||||||
<Image
|
.then((response) => response.json())
|
||||||
src={"/images/logo.svg"}
|
.then((result: Category[]) => {
|
||||||
height={40}
|
setCategories(result);
|
||||||
width={160}
|
});
|
||||||
alt="Nav bar logo"
|
}
|
||||||
onClick={() => {
|
|
||||||
window.open("/", "_self");
|
|
||||||
}}
|
|
||||||
className={styles.logo}
|
|
||||||
/>
|
|
||||||
<div className={styles.links}>
|
|
||||||
<div className={styles.dropDown}>
|
|
||||||
<Link href="/articles">Categories</Link>
|
|
||||||
<div className={styles.dropDownContainer}>
|
|
||||||
<div className={styles.content}>
|
|
||||||
<Link href={"/articles"}>All</Link>
|
|
||||||
|
|
||||||
{categories?.map((cat, i) => {
|
getCategories();
|
||||||
{
|
}, []);
|
||||||
return (
|
return (
|
||||||
<Link
|
<nav className={styles.nav}>
|
||||||
key={i}
|
<div className={styles.containerLeft}>
|
||||||
href={`/articles/${cat.name.toLowerCase()}`}
|
<Image
|
||||||
>
|
src={"/images/logo.svg"}
|
||||||
{cat.title}
|
height={40}
|
||||||
</Link>
|
width={160}
|
||||||
);
|
alt="Nav bar logo"
|
||||||
}
|
onClick={() => {
|
||||||
})}
|
window.open("/", "_self");
|
||||||
</div>
|
}}
|
||||||
</div>
|
className={styles.logo}
|
||||||
</div>
|
/>
|
||||||
</div>
|
<div className={styles.links}>
|
||||||
</div>
|
<div className={styles.dropDown}>
|
||||||
<div className={styles.containerCenter}>
|
<Link href="/articles">Categories</Link>
|
||||||
<div className={styles.searchBar}>
|
<div className={styles.dropDownContainer}>
|
||||||
<div className={styles.icon}>
|
<div className={styles.content}>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
<Link href={"/articles"}>All</Link>
|
||||||
<path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352c79.5 0 144-64.5 144-144s-64.5-144-144-144S64 128.5 64 208s64.5 144 144 144z" />
|
|
||||||
</svg>
|
{categories?.map((cat, i) => {
|
||||||
</div>
|
{
|
||||||
<input onInput={handleSearchInput} type="text" name="" id="" />
|
return (
|
||||||
</div>
|
<Link key={i} href={`/articles/${cat.name.toLowerCase()}`}>
|
||||||
<div className={styles.searchResults}>
|
{cat.title}
|
||||||
<div className={styles.content}>
|
</Link>
|
||||||
{searchResults.map((s) => {
|
);
|
||||||
{
|
}
|
||||||
return (
|
})}
|
||||||
<Link href={`/articles/${s.name.toLowerCase()}`}>
|
</div>
|
||||||
{s.title}
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
</div>
|
||||||
})}
|
<div className={styles.containerCenter}>
|
||||||
</div>
|
<div className={styles.searchBar}>
|
||||||
</div>
|
<div className={styles.icon}>
|
||||||
</div>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
<div className={styles.containerRight}>
|
<path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352c79.5 0 144-64.5 144-144s-64.5-144-144-144S64 128.5 64 208s64.5 144 144 144z" />
|
||||||
<div
|
</svg>
|
||||||
className={styles.themeSwitch}
|
</div>
|
||||||
onClick={() => {
|
<input onInput={handleSearchInput} type="text" name="" id="" />
|
||||||
toggleTheme();
|
</div>
|
||||||
}}
|
<div className={styles.searchResults}>
|
||||||
>
|
<div className={styles.content}>
|
||||||
<svg
|
{searchResults.map((s) => {
|
||||||
id="themeSwitchSvg"
|
{
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
return <Link href={`/articles/${s.name.toLowerCase()}`}>{s.title}</Link>;
|
||||||
viewBox="0 0 512 512"
|
}
|
||||||
>
|
})}
|
||||||
<path d="M448 256c0-106-86-192-192-192V448c106 0 192-86 192-192zm64 0c0 141.4-114.6 256-256 256S0 397.4 0 256S114.6 0 256 0S512 114.6 512 256z" />
|
</div>
|
||||||
</svg>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className={styles.containerRight}>
|
||||||
</nav>
|
<div
|
||||||
);
|
className={styles.themeSwitch}
|
||||||
|
onClick={() => {
|
||||||
|
toggleTheme();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg id="themeSwitchSvg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<path d="M448 256c0-106-86-192-192-192V448c106 0 192-86 192-192zm64 0c0 141.4-114.6 256-256 256S0 397.4 0 256S114.6 0 256 0S512 114.6 512 256z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import type { AppProps } from "next/app";
|
|||||||
import AdminNav from "@/components/AdminNav";
|
import AdminNav from "@/components/AdminNav";
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
import Nav from "@/components/Nav";
|
import Nav from "@/components/Nav";
|
||||||
|
import { Category } from "@prisma/client";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header>
|
<header>
|
||||||
<Nav categories={[]} />
|
<Nav />
|
||||||
<AdminNav />
|
<AdminNav />
|
||||||
</header>
|
</header>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { Html, Head, Main, NextScript } from "next/document";
|
|||||||
|
|
||||||
export default function Document() {
|
export default function Document() {
|
||||||
return (
|
return (
|
||||||
<Html lang="en">
|
<Html lang="en" style={{ scrollBehavior: "smooth" }}>
|
||||||
<Head />
|
<Head />
|
||||||
<body>
|
<body>
|
||||||
<Main />
|
<Main />
|
||||||
|
|
||||||
<NextScript />
|
<NextScript />
|
||||||
</body>
|
</body>
|
||||||
</Html>
|
</Html>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function AdminArticlesEditorPage({ article, categories }: { artic
|
|||||||
function changeContentTableEntryTitle(index: number, newTitle: string) {
|
function changeContentTableEntryTitle(index: number, newTitle: string) {
|
||||||
setContentTable((prevArray: any) => {
|
setContentTable((prevArray: any) => {
|
||||||
let newArray = [...prevArray];
|
let newArray = [...prevArray];
|
||||||
newArray[index].anchor = newTitle;
|
newArray[index].title = newTitle;
|
||||||
return newArray;
|
return newArray;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { Prisma, Article } from "@prisma/client";
|
import { Prisma, Article } from "@prisma/client";
|
||||||
import { ResponseError } from "../../../types/responseErrors";
|
import { ResponseError } from "../../../types/responseErrors";
|
||||||
import { formatTextToUrlName } from "../../../utils";
|
import { formatTextToUrlName } from "../../../utils";
|
||||||
import prisma from "../../../lib/prisma";
|
import prisma, { ArticleWithIncludes } from "../../../lib/prisma";
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { UpdateArticle } from "../../../types/api";
|
import { UpdateArticle } from "../../../types/api";
|
||||||
import { isValidText } from "../../../validators";
|
import { isValidText } from "../../../validators";
|
||||||
|
|
||||||
type ArticleWithIncludes = Prisma.ArticleGetPayload<{ include: { contentTableEntries: true, category: true, image: true } }>
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const articleId: string = formatTextToUrlName(req.query?.articleId?.toString() ?? "")
|
||||||
|
|
||||||
if (req.method == "PUT") {//* PUT
|
if (req.method == "PUT") {//* PUT
|
||||||
console.log("PUT")
|
console.log("PUT")
|
||||||
const articleId: string = formatTextToUrlName(req.query?.articleId?.toString() ?? "")
|
|
||||||
|
|
||||||
console.log(`API articleId: ${articleId}`)
|
console.log(`API articleId: ${articleId}`)
|
||||||
const articleData: UpdateArticle = req.body;
|
const articleData: UpdateArticle = req.body;
|
||||||
@@ -55,6 +55,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).end();
|
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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import prisma from "../../../lib/prisma";
|
import prisma, { CategoryWithIncludes } from "../../../lib/prisma";
|
||||||
import { Category } from "@prisma/client";
|
import { Category } from "@prisma/client";
|
||||||
import { ResponseError } from "../../../types/responseErrors";
|
import { ResponseError } from "../../../types/responseErrors";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
@@ -9,10 +9,10 @@ import { isValidText } from "../../../validators";
|
|||||||
import { UpdateCategory } from '../../../types/api';
|
import { UpdateCategory } from '../../../types/api';
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const categoryId: string = req.query.categoryId?.toString() ?? "";
|
||||||
|
|
||||||
if (req.method == "PUT") {
|
if (req.method == "PUT") {
|
||||||
const categoryId: string = req.query.categoryId?.toString() ?? "";
|
|
||||||
const categoryData: UpdateCategory = req.body;
|
const categoryData: UpdateCategory = req.body;
|
||||||
|
|
||||||
if (categoryData.title && !isValidText(categoryData.title)) {
|
if (categoryData.title && !isValidText(categoryData.title)) {
|
||||||
@@ -66,5 +66,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).end();
|
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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,66 +10,77 @@ import type { NextApiRequest, NextApiResponse } from 'next'
|
|||||||
import { CreateCategory } from "@/types/api";
|
import { CreateCategory } from "@/types/api";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method == "POST") {
|
if (req.method == "GET") {
|
||||||
console.log("API new category")
|
console.log("API get categories")
|
||||||
const categoryData: CreateCategory = req.body;
|
await prisma.category.findMany().then((result: Category[]) => {
|
||||||
console.log(categoryData)
|
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)) {
|
if (!isValidText(categoryData.title)) {
|
||||||
res.json({ target: "title", error: "Not a valid title" });
|
res.json({ target: "title", error: "Not a valid title" });
|
||||||
return;
|
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 = {
|
const newSvg: Prisma.SvgUncheckedCreateInput = {
|
||||||
viewbox: categoryData.svg.viewbox,
|
viewbox: categoryData.svg.viewbox,
|
||||||
path: categoryData.svg.path
|
path: categoryData.svg.path
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.svg
|
await prisma.svg
|
||||||
.create({ data: newSvg })
|
.create({ data: newSvg })
|
||||||
.then(
|
.then(
|
||||||
async (createdSvg: Svg) => {
|
async (createdSvg: Svg) => {
|
||||||
const newCategory: Prisma.CategoryUncheckedCreateInput = {
|
const newCategory: Prisma.CategoryUncheckedCreateInput = {
|
||||||
title: categoryData.title,
|
title: categoryData.title,
|
||||||
name: formatTextToUrlName(categoryData.title),
|
name: formatTextToUrlName(categoryData.title),
|
||||||
color: categoryData.color ?? "teal",
|
color: categoryData.color ?? "teal",
|
||||||
svgId: createdSvg.id,
|
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
|
.catch((err) => {
|
||||||
.create({
|
console.error(err);
|
||||||
data: newCategory,
|
res.status(500).end();
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
30
pages/api/search.ts
Normal file
30
pages/api/search.ts
Normal file
@@ -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([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,9 @@ import Image from "next/image";
|
|||||||
import Markdown from "@/components/Markdown";
|
import Markdown from "@/components/Markdown";
|
||||||
import { formatTextToUrlName } from "@/utils";
|
import { formatTextToUrlName } from "@/utils";
|
||||||
import prisma, { ArticleWithIncludes, CategoryWithIncludes } from "@/lib/prisma";
|
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
|
//* MAIN
|
||||||
export default function ArticlePage({ article }: { article: ArticleWithIncludes }) {
|
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" };
|
const dateOptions: Intl.DateTimeFormatOptions = { month: "long", day: "numeric", year: "numeric" };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.article}>
|
<>
|
||||||
<ContentTable contentTableData={article?.contentTable ? article?.contentTable : []} />
|
<ArticleControl articleId={article.id} />
|
||||||
<div className={styles.tutorialContent}>
|
<div className={styles.article}>
|
||||||
<div className={styles.header}>
|
<ContentTable
|
||||||
<p className={`${styles.dates} text-muted`}>
|
contentTableData={article?.contentTable ? Array.from(article.contentTable as Prisma.JsonArray).map((c: any) => ({ anchor: c.anchor, title: c.title })) : []}
|
||||||
{`Published on ${dateCreated.toLocaleDateString("en-US", dateOptions)}`}
|
/>
|
||||||
<br />
|
<div className={styles.tutorialContent}>
|
||||||
{dateUpdated > dateCreated ? `Updated on ${dateUpdated.toLocaleDateString("en-US", dateOptions)}` : ""}
|
<div className={styles.header}>
|
||||||
</p>
|
<p className={`${styles.dates} text-muted`}>
|
||||||
|
{`Published on ${dateCreated.toLocaleDateString("en-US", dateOptions)}`}
|
||||||
|
<br />
|
||||||
|
{dateUpdated > dateCreated ? `Updated on ${dateUpdated.toLocaleDateString("en-US", dateOptions)}` : ""}
|
||||||
|
</p>
|
||||||
|
|
||||||
<h1>{article?.title}</h1>
|
<h1>{article?.title}</h1>
|
||||||
<div className={styles.tags}>
|
<div className={styles.tags}>
|
||||||
<a href="#">Docker</a> <a href="#">Setup</a> <a href="#">Ubuntu</a>
|
<a href="#">Docker</a> <a href="#">Setup</a> <a href="#">Ubuntu</a>
|
||||||
|
</div>
|
||||||
|
<Image src={""} height={350} width={750} alt={""} quality={100} placeholder="blur" blurDataURL="/images/blur.png" loading="lazy" />
|
||||||
|
<p>{article?.introduction}</p>
|
||||||
</div>
|
</div>
|
||||||
<Image src={""} height={350} width={750} alt={""} quality={100} placeholder="blur" blurDataURL="/images/blur.png" loading="lazy" />
|
<Markdown value={article?.markdown ?? ""} />
|
||||||
<p>{article?.introduction}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Markdown value={article?.markdown ?? ""} />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
<Sidebar />
|
</>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,18 @@ import Link from "next/link";
|
|||||||
import { formatTextToUrlName } from "@/utils";
|
import { formatTextToUrlName } from "@/utils";
|
||||||
import { Article, Category } from "@prisma/client";
|
import { Article, Category } from "@prisma/client";
|
||||||
import prisma, { CategoryWithIncludes } from "@/lib/prisma";
|
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 (
|
return (
|
||||||
<div className={styles.category}>
|
<>
|
||||||
<h1>{category?.title}</h1>
|
<CategoryControl categoryId={category.id} />
|
||||||
<div className={styles.content}>
|
<div className={styles.category}>
|
||||||
<div className={`${styles.showcase} ${styles.smallShowcase}`}>
|
<h1>{category?.title}</h1>
|
||||||
<h2>Most popular articles</h2>
|
<div className={styles.content}>
|
||||||
{/* {popularArticles?.map((a, i) => {
|
<div className={`${styles.showcase} ${styles.smallShowcase}`}>
|
||||||
|
<h2>Most popular articles</h2>
|
||||||
|
{/* {popularArticles?.map((a, i) => {
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Link key={i} href={`/articles/${category.name}/${a.name}`}>
|
<Link key={i} href={`/articles/${category.name}/${a.name}`}>
|
||||||
@@ -20,9 +23,9 @@ export default function CategoryPage({ category }: { category: CategoryWithInclu
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
})} */}
|
})} */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <div className={`${styles.showcase} ${styles.smallShowcase}`}>
|
{/* <div className={`${styles.showcase} ${styles.smallShowcase}`}>
|
||||||
<h2>Most recent articles</h2>
|
<h2>Most recent articles</h2>
|
||||||
{recentArticles?.map((a, i) => {
|
{recentArticles?.map((a, i) => {
|
||||||
{
|
{
|
||||||
@@ -35,22 +38,23 @@ export default function CategoryPage({ category }: { category: CategoryWithInclu
|
|||||||
})}
|
})}
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
<div className={styles.showcase}>
|
<div className={styles.showcase}>
|
||||||
<h2>All articles</h2>
|
<h2>All articles</h2>
|
||||||
{category?.articles
|
{category?.articles
|
||||||
? Array.from(category?.articles).map((a: Article, i: number) => {
|
? Array.from(category?.articles).map((a: Article, i: number) => {
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Link key={i} href={`/articles/${category.name}/${a.name}`}>
|
<Link key={i} href={`/articles/${category.name}/${a.name}`}>
|
||||||
{a.title}
|
{a.title}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
: ""}
|
: ""}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export async function getServerSideProps(context: any) {
|
export async function getServerSideProps(context: any) {
|
||||||
|
|||||||
5
prisma/migrations/20230207142155_ondelete/migration.sql
Normal file
5
prisma/migrations/20230207142155_ondelete/migration.sql
Normal file
@@ -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;
|
||||||
@@ -16,7 +16,7 @@ model Article {
|
|||||||
markdown String
|
markdown String
|
||||||
contentTable Json?
|
contentTable Json?
|
||||||
categoryId String
|
categoryId String
|
||||||
category Category @relation(fields: [categoryId], references: [id])
|
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
||||||
dateCreated DateTime @default(now())
|
dateCreated DateTime @default(now())
|
||||||
dateUpdated DateTime @default(now())
|
dateUpdated DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user