This commit is contained in:
Janis
2023-02-07 13:45:40 +01:00
parent d5f5b79140
commit d2ff34d3b6
70 changed files with 1388 additions and 8768 deletions

21
pages/_app.tsx Normal file
View File

@@ -0,0 +1,21 @@
import "@/styles/globals.scss";
import "@/styles/inputs.scss";
import "@/styles/buttons.scss";
import "@/styles/typography.scss";
import type { AppProps } from "next/app";
import AdminNav from "@/components/AdminNav";
import Footer from "@/components/Footer";
import Nav from "@/components/Nav";
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<header>
<Nav categories={[]} />
<AdminNav />
</header>
<Component {...pageProps} />
<Footer />
</>
);
}

14
pages/_document.tsx Normal file
View File

@@ -0,0 +1,14 @@
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

View File

@@ -0,0 +1,285 @@
import React from "react";
import { useState, useRef, useEffect } from "react";
import styles from "@/styles/modules/ArticleEditor.module.scss";
import { Prisma, Category } from "@prisma/client";
import Select from "react-select";
import { useRouter } from "next/navigation";
import urlJoin from "url-join";
import { IContentTableEntry } from "@/types/contentTable";
import { CreateArticle, UpdateArticle } from "@/types/api";
import { formatTextToUrlName } from "@/utils";
import { isValidText } from "@/validators";
import { apiUrl } from "@/global";
import Markdown from "@/components/Markdown";
import prisma, { ArticleWithIncludes, CategoryWithIncludes } from "@/lib/prisma";
import { CLIENT_RENEG_LIMIT } from "tls";
export default function AdminArticlesEditorPage({ article, categories }: { article: ArticleWithIncludes | null; categories: CategoryWithIncludes[] }) {
const router = useRouter();
const [title, setTitle] = useState<string>(article?.title ?? "");
const [selectCategoriesOptions, setSelectCategoriesOptions] = useState<{ value: string; label: string }[]>(
categories?.map((c: CategoryWithIncludes) => ({ value: c.id, label: c.title }))
);
const [introduction, setIntroduction] = useState<string>(article?.introduction ?? "");
const [markdown, setMarkdown] = useState<string>(article?.markdown ?? "");
const [contentTable, setContentTable] = useState<any>(article?.contentTable ?? []);
const titleRef = useRef<HTMLInputElement>(null);
const categorySelectRef = useRef<any>(null);
const introductionRef = useRef<HTMLInputElement>(null);
const markdownTextAreaRef = useRef<HTMLTextAreaElement>(null);
const errorTextRef = useRef<HTMLParagraphElement>(null);
function changeContentTableEntryAnchor(index: number, newAnchor: string) {
setContentTable((prevArray: any) => {
let newArray = [...prevArray];
newArray[index].anchor = newAnchor;
return newArray;
});
}
function changeContentTableEntryTitle(index: number, newTitle: string) {
setContentTable((prevArray: any) => {
let newArray = [...prevArray];
newArray[index].anchor = newTitle;
return newArray;
});
}
function removeEntry(index: number) {
let newArray = [...contentTable];
newArray.splice(index, 1);
setContentTable(newArray);
}
// Create or update article
async function handleResponse(res: Response) {
const json = await res.json();
if (errorTextRef?.current) {
errorTextRef.current.innerText = json.error ?? "";
}
if (json.success) {
const newArticle: ArticleWithIncludes = json.data;
router.push(urlJoin(`/articles/`, newArticle.category.name, newArticle.name));
}
}
async function updateArticle() {
console.log("Update article");
const payload: UpdateArticle = {
title: titleRef?.current?.value,
introduction: introductionRef?.current?.value,
markdown: markdown,
categoryId: categorySelectRef?.current?.getValue()[0]?.value,
contentTable: contentTable,
};
console.log(payload);
await fetch(`/api/articles/${article?.id.toString()}`, {
method: "PUT",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
cache: "no-cache",
body: JSON.stringify(payload),
})
.then(handleResponse)
.catch(console.error);
}
async function createArticle() {
console.log("Create article");
const payload: CreateArticle = {
title: titleRef?.current?.value ?? "",
introduction: introductionRef?.current?.value ?? "",
markdown: markdown,
categoryId: categorySelectRef?.current?.getValue()[0]?.value,
contentTable: contentTable,
};
console.log(payload);
await fetch("/api/articles/", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
cache: "no-cache",
body: JSON.stringify(payload),
})
.then(handleResponse)
.catch(console.error);
}
return (
<div className={styles.adminArticlesCreate}>
<h1>{article ? "Update article" : "Create new article"}</h1>
<div className={styles.formControl}>
<p className="text-error" ref={errorTextRef}></p>
<button
type="button"
onClick={() => {
if (article) {
updateArticle();
} else {
createArticle();
}
}}
>
{article ? "Update article" : "Create article"}
</button>
</div>
<div className={styles.form}>
<div className={styles.articleEditor}>
<div className={styles.title}>
<label htmlFor="title">Title</label>
<div className={styles.titleInputs}>
<input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTitle(e.target.value);
}}
className={!isValidText(title) && title ? "error" : ""}
type="text"
name="title"
placeholder="title"
ref={titleRef}
defaultValue={title}
/>
<input readOnly={true} className={""} type="text" name="name" value={title ? formatTextToUrlName(title) : ""} />
</div>
</div>
<div className={styles.category}>
<label htmlFor="title">Category</label>
<Select
ref={categorySelectRef}
className="react-select-container"
classNamePrefix="react-select"
options={selectCategoriesOptions}
defaultValue={article ? { value: article.category.id, label: article.category.title } : {}}
/>
</div>
<div className={styles.introduction}>
<label htmlFor="title">Introduction</label>
<input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setIntroduction(e.target.value);
}}
className={!isValidText(introduction) && introduction ? "error" : ""}
type="text"
name="introduction"
placeholder="Introduction"
ref={introductionRef}
defaultValue={introduction}
/>
</div>
<div className={styles.markdown}>
<label htmlFor="">Markdown Editor</label>
<div className={styles.markdownEditor}>
<textarea
ref={markdownTextAreaRef}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
setMarkdown(e.target.value);
}}
defaultValue={markdown}
></textarea>
<Markdown value={markdown} />
</div>
</div>
<div className={styles.contentTable}>
<label htmlFor="">Table of contents</label>
<div className={styles.contentTableEditor}>
<div className={styles.entries}>
{contentTable?.map((entry: IContentTableEntry, i: number) => {
return (
<div key={i}>
<input
onChange={(e) => {
changeContentTableEntryAnchor(i, e.target.value);
}}
type="text"
placeholder={"Anchor"}
defaultValue={entry.anchor}
/>
<input
onChange={(e) => {
changeContentTableEntryTitle(i, e.target.value);
}}
type="text"
placeholder={"Title"}
defaultValue={entry.title}
/>{" "}
<button
onClick={() => {
removeEntry(i);
}}
>
Remove
</button>
</div>
);
})}
<button
onClick={() => {
setContentTable([...contentTable, { title: "", anchor: "" }]);
}}
>
Add
</button>
</div>
<Markdown value={markdown} />
</div>
</div>
</div>
</div>
</div>
);
}
export async function getServerSideProps(context: any) {
let article: ArticleWithIncludes | null = null;
let categories: CategoryWithIncludes[] = [];
const articleId: string = context.params.articleId.toString();
if (articleId != "0") {
await prisma.article.findUnique({ where: { id: articleId }, include: { category: true } }).then(
(result: ArticleWithIncludes | null) => {
if (result) {
article = JSON.parse(JSON.stringify(result));
console.log(article);
} else {
// TODO redirect to /0
}
},
(reason: any) => {
console.log(reason);
}
);
}
await prisma.category.findMany({ include: { svg: true, articles: true } }).then(
(result: CategoryWithIncludes[]) => {
if (result) {
categories = JSON.parse(JSON.stringify(result));
} else {
// TODO redirect to /0
}
},
(reason: any) => {
console.log(reason);
}
);
return {
props: { article: article, categories: categories }, // will be passed to the page component as props
};
}

View File

@@ -0,0 +1,186 @@
import React, { useRef, useState } from "react";
import styles from "@/styles/modules/CategoryEditor.module.scss";
import { Prisma, Category } from "@prisma/client";
import { formatTextToUrlName } from "@/utils";
import { isValidText } from "@/validators";
import { CreateCategory, UpdateCategory } from "@/types/api";
import urlJoin from "url-join";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { apiUrl } from "@/global";
import prisma, { CategoryWithIncludes } from "@/lib/prisma";
export default function AdminCategoriesEditor({ category }: { category: CategoryWithIncludes | null }) {
const router = useRouter();
const [title, setTitle] = useState<string>(category?.title ?? "");
const [color, setColor] = useState<string>(category?.color ?? "");
const [svgViewbox, setSvgViewbox] = useState<string>(category?.svg?.viewbox ?? "");
const [svgPath, setSvgPath] = useState<string>(category?.svg?.path ?? "");
const titleRef = useRef<HTMLInputElement>(null);
const colorRef = useRef<HTMLInputElement>(null);
const svgViewboxRef = useRef<HTMLInputElement>(null);
const svgPathRef = useRef<HTMLInputElement>(null);
const errorTextRef = useRef<HTMLParagraphElement>(null);
async function handleResponse(res: Response) {
const json = await res.json();
if (errorTextRef?.current) {
errorTextRef.current.innerText = json.error ?? "";
}
if (json.success) {
router.push(urlJoin(`/articles/`));
}
}
async function updateCategory() {
console.log("Update category");
const payload: UpdateCategory = {
title: titleRef?.current?.value,
color: colorRef?.current?.value,
svg: {
path: svgPathRef?.current?.value,
viewbox: svgViewboxRef?.current?.value,
},
};
console.log(payload);
await fetch(`/api/categories/${category?.id.toString()}`, {
method: "PUT",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
cache: "no-cache",
body: JSON.stringify(payload),
})
.then(handleResponse)
.catch(console.error);
}
async function createCategory() {
console.log("Create category");
const payload: CreateCategory = {
title: titleRef?.current?.value ?? "",
color: colorRef?.current?.value ?? "",
svg: {
path: svgPathRef?.current?.value ?? "",
viewbox: svgViewboxRef?.current?.value ?? "",
},
};
console.log(payload);
await fetch("/api/categories/", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
cache: "no-cache",
body: JSON.stringify(payload),
})
.then(handleResponse)
.catch(console.error);
}
return (
<div className={styles.categoryEditor}>
<h1>{category ? "Update category" : "Create new category"}</h1>
<div className={styles.formControl}>
<p className="text-error" ref={errorTextRef}></p>
<button
type="button"
onClick={() => {
if (category) {
updateCategory();
} else {
createCategory();
}
}}
>
{category ? "Update category" : "Create category"}
</button>
</div>
<div className={styles.form}>
<div className={styles.title}>
<label htmlFor="title">Title</label>
<div className={styles.titleInputs}>
<input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTitle(e.target.value);
}}
className={!isValidText(title) && title ? "error" : ""}
type="text"
name="title"
placeholder="title"
ref={titleRef}
defaultValue={title}
/>
<input readOnly={true} className={""} type="text" name="name" value={title ? formatTextToUrlName(title) : ""} />
</div>
<div className={styles.svg}>
<label>SVG</label>
<div className={styles.svgInputs}>
<input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSvgPath(e.target.value);
}}
type="text"
placeholder="svg path"
ref={svgPathRef}
defaultValue={svgPath}
/>
<input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSvgViewbox(e.target.value);
}}
type="text"
placeholder="0 0 512 512"
ref={svgViewboxRef}
defaultValue={svgViewbox}
/>
</div>
</div>
<div className={styles.color}>
<label>Color</label>
<input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setColor(e.target.value);
}}
type="color"
ref={colorRef}
defaultValue={color}
/>
</div>
</div>
</div>
</div>
);
}
export async function getServerSideProps(context: any) {
let category: CategoryWithIncludes | null = null;
const categoryId: string = context.params.categoryId.toString();
if (categoryId != "0") {
await prisma.category.findUnique({ where: { id: categoryId }, include: { articles: true, svg: true } }).then(
(result: CategoryWithIncludes | null) => {
if (result) {
category = JSON.parse(JSON.stringify(result));
} else {
// TODO redirect to /0
}
},
(reason: any) => {
console.log(reason);
}
);
}
return {
props: { category: category }, // will be passed to the page component as props
};
}

View File

@@ -10,64 +10,33 @@ 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())
console.log(`API articleId: ${articleId}`)
if (req.method == "GET") { //* GET
console.log("get")
await prisma.article
.findUnique({ where: { id: articleId }, include: { category: true, image: true } })
.then((result: ArticleWithIncludes) => {
if (result !== null) {
console.log("result", result)
res.json(result);
} else {
console.log("no article found")
const error: ResponseError = {
code: "404",
message: "No article with this name found!",
};
res.status(404).json(error);
}
}, (err) => {
console.log("reason", err)
})
.catch((err) => {
console.log("catch", err)
const error: ResponseError = {
code: "500",
message: err,
};
res.status(500).json(error);
});
} else if (req.method == "PUT") {//* PUT
if (req.method == "PUT") {//* PUT
console.log("PUT")
const data: UpdateArticle = req.body;
const articleId: string = formatTextToUrlName(req.query?.articleId?.toString() ?? "")
if (!isValidText(data.title)) {
console.log(`API articleId: ${articleId}`)
const articleData: UpdateArticle = req.body;
if (articleData.title && !isValidText(articleData.title)) {
res.json({ target: "title", error: "Not a valid title" });
return;
}
if (!isValidText(data.introduction)) {
if (articleData.introduction && !isValidText(articleData.introduction)) {
res.json({ target: "introduction", error: "Not a valid introduction" });
return;
}
if (!data.categoryId) {
res.json({ target: "category", error: "Category is required" });
return;
}
const newArticle: Prisma.ArticleUncheckedUpdateInput = {
title: data.title ?? undefined,
name: formatTextToUrlName(data.title) ?? undefined,
introduction: data.introduction ?? undefined,
title: articleData.title ?? undefined,
name: articleData.title ? formatTextToUrlName(articleData.title) : undefined,
introduction: articleData.introduction ?? undefined,
categoryId: data.categoryId?.toString() ?? undefined,
contentTable: data.contentTable ?? undefined,
markdown: data.markdown ?? undefined,
imageId: data.imageId?.toString() ?? undefined,
categoryId: articleData.categoryId ?? undefined,
contentTable: articleData.contentTable ?? undefined,
markdown: articleData.markdown ?? undefined,
imageUrl: articleData.imageUrl ?? undefined,
}
console.log(newArticle)
await prisma.article.update({ data: newArticle, where: { id: articleId }, include: { category: true } })

View File

@@ -1,92 +1,48 @@
import prisma from "../../../lib/prisma";
import { Prisma } from "@prisma/client";
import { Article, Category } from "@prisma/client";
import { ResponseError } from "../../../types/responseErrors";
import { CreateArticle } from "@/types/api";
import prisma, { ArticleWithIncludes } from "../../../lib/prisma";
import { formatTextToUrlName } from "../../../utils";
import { isValidText } from "../../../validators";
import { title } from 'process';
import { UpdateArticle } from "../../../types/api";
import type { NextApiRequest, NextApiResponse } from 'next'
import { Prisma } from '@prisma/client';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
console.log("articles index.ts")
if (req.method == "GET") { //* GET
console.log("get")
const categoryName: string = req.query.categoryName?.toString() ?? "";
const limit: number = req.query.limit ? Number(req.query.limit) : undefined;
const orderBy: string = req.query.orderBy?.toString() ?? "";
const category = await prisma.category.findUnique({ where: { name: categoryName } });
console.log(categoryName, limit, orderBy, category)
let orderByObj: Prisma.Enumerable<Prisma.ArticleOrderByWithRelationInput>;
if (orderBy === "recent") {
orderByObj = {
dateCreated: "desc"
}
} else if (orderBy === "popularity") {
// TODO filter with views
}
await prisma.article
.findMany({
where: { category: category ?? undefined },
include: { category: true },
take: limit,
orderBy: orderByObj
})
.then((result: Article[]) => { //! ContentTableEntries not sorted
console.log("result", result)
if (result !== null) {
res.end(JSON.stringify(result));
} else {
const error: ResponseError = {
code: "404",
message: "No articles found!",
};
res.status(404).json(error);
}
}, (err) => {
console.log("reason", err)
})
.catch((err) => {
console.log("catch", err)
const error: ResponseError = {
code: "500",
message: err,
};
res.status(500).json(JSON.stringify(error));
});
if (req.method == "POST") { //* POST
console.log("API new article")
const articleData: CreateArticle = req.body
console.log(articleData)
} else if (req.method == "POST") { //* POST
const data: any = req.body;
console.log(data)
if (!isValidText(data.title)) {
res.json({ target: "title", error: "Not a valid title" });
if (!isValidText(articleData.title)) {
res.status(500).json({ target: "title", error: "Not a valid title" });
return;
}
if (!isValidText(data.introduction)) {
res.json({ target: "introduction", error: "Not a valid introduction" });
if (!isValidText(articleData.introduction)) {
res.status(500).json({ target: "introduction", error: "Not a valid introduction" });
return;
}
if (!data.categoryId) {
res.json({ target: "category", error: "Category is required" });
if (!articleData.categoryId) {
res.status(500).json({ target: "category", error: "Category is required" });
return;
}
data.name = formatTextToUrlName(data.title);
data.categoryId = data.categoryId.toString();
const newArticle: Prisma.ArticleUncheckedCreateInput = {
title: articleData.title,
name: formatTextToUrlName(articleData.title),
introduction: articleData.introduction,
categoryId: articleData.categoryId,
markdown: articleData.markdown ?? "",
contentTable: articleData.contentTable ?? {},
imageUrl: articleData.imageUrl ?? ""
}
prisma.article
.create({ data: data, include: { category: true } })
.create({ data: newArticle, include: { category: true } })
.then(
(data) => {
console.log("success")
(data: ArticleWithIncludes) => {
res.json({ success: true, data: data });
},
(errorReason) => {

View File

@@ -1,39 +0,0 @@
import prisma from "../../../../lib/prisma";
import { Prisma } from '@prisma/client';
import { ResponseError } from "../../../../types/responseErrors";
import { formatTextToUrlName } from "../../../../utils";
import type { NextApiRequest, NextApiResponse } from 'next'
type ArticleWithIncludes = Prisma.ArticleGetPayload<{ include: { category: true, image: true } }>
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const articleName: string = formatTextToUrlName(req.query.articleName.toString())
console.log(`API: articleName: ${articleName}`)
await prisma.article
.findUnique({ where: { name: articleName }, include: { category: true, image: true } })
.then((result: ArticleWithIncludes) => {
console.log("result", result)
if (result !== null) {
console.log("send")
res.json(result);
} else {
console.log("response no article found")
const error: ResponseError = {
code: "404",
message: "No article with this name found!",
};
res.status(404).json(error);
}
}, (err) => {
console.log("reason", err)
})
.catch((err) => {
console.log("catch", err)
const error: ResponseError = {
code: "500",
message: err,
};
res.status(500).json(error);
});
}

View File

@@ -1,4 +1,3 @@
import { Request, Response } from "express";
import prisma from "../../../lib/prisma";
import { Category } from "@prisma/client";
import { ResponseError } from "../../../types/responseErrors";
@@ -7,51 +6,29 @@ import { Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from 'next'
import { formatTextToUrlName } from "../../../utils";
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() ?? undefined;
if (req.method == "GET") {
await prisma.category
.findUnique({ where: { id: categoryId }, include: { svg: true } })
.then((result: Category) => {
if (result !== null) {
res.json(result);
} else {
const error: ResponseError = {
code: "404",
message: "No category with this id found!",
};
res.status(404).json(error);
}
})
.catch((err) => {
const error: ResponseError = {
code: "500",
message: err,
};
res.status(500).json(error);
});
} else if (req.method == "PUT") {
const data: any = req.body;
if (!isValidText(data.title)) {
if (req.method == "PUT") {
const categoryId: string = req.query.categoryId?.toString() ?? "";
const categoryData: UpdateCategory = req.body;
if (categoryData.title && !isValidText(categoryData.title)) {
res.json({ target: "title", error: "Not a valid title" });
return;
}
data.name = formatTextToUrlName(data.title);
console.log(data);
const newSvg: Prisma.SvgUncheckedUpdateInput = {
viewbox: data.svg.viewbox,
path: data.svg.path,
viewbox: categoryData.svg?.viewbox ?? undefined,
path: categoryData.svg?.path ?? undefined,
};
const newCategory: Prisma.CategoryUncheckedUpdateInput = {
title: data.title,
name: data.name,
color: data.color,
title: categoryData.title ?? undefined,
name: categoryData.title ? formatTextToUrlName(categoryData.title) : undefined,
color: categoryData.color ?? undefined,
};
await prisma.category
@@ -62,7 +39,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
})
.then(
async (categoryData) => {
console.log("2");
await prisma.svg
.update({ data: newSvg, where: { id: categoryData.svg.id } })
.then(

View File

@@ -1,5 +1,5 @@
import prisma from "../../../lib/prisma";
import prisma, { CategoryWithIncludes } from "../../../lib/prisma";
import { Prisma } from "@prisma/client";
import { Category, Svg } from "@prisma/client";
import { ResponseError } from "../../../types/responseErrors";
@@ -7,48 +7,52 @@ import { formatTextToUrlName } from "../../../utils";
import { isValidText } from "../../../validators";
import type { NextApiRequest, NextApiResponse } from 'next'
import { CreateCategory } from "@/types/api";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method == "GET") { //* GET
await prisma.category
.findMany({ include: { svg: true } })
.then((result: Category[]) => {
if (result !== null) {
res.json(result);
} else {
console.log("No categories found");
res.json([]);
}
})
.catch((err) => {
console.log(err);
res.json([]);
});
} else if (req.method == "POST") {
const data: any = req.body;
if (!isValidText(data.title)) {
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;
}
data.name = formatTextToUrlName(data.title);
data.svg.viewbox = data.svg.viewbox.length > 1 ? data.svg.viewbox : null;
console.log(data);
categoryData.svg.viewbox = categoryData.svg.viewbox.length > 1 ? categoryData.svg.viewbox : "";
const newSvg: Prisma.SvgUncheckedCreateInput = {
viewbox: categoryData.svg.viewbox,
path: categoryData.svg.path
}
await prisma.svg
.create({ data: data.svg })
.create({ data: newSvg })
.then(
async (svgData) => {
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: { title: data.title, name: data.name, color: data.name, svgId: svgData.id },
include: { svg: true },
data: newCategory,
include: { svg: true, articles: true },
})
.then(
(data) => {
res.json({ success: true, data: data });
(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" });
}

View File

@@ -1,29 +0,0 @@
import { Category } from "@prisma/client";
import prisma from "../../../../lib/prisma";
import { ResponseError } from "../../../../types/responseErrors";
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const categoryName: string = req.query.categoryName.toString() ?? undefined;
await prisma.category
.findUnique({ where: { name: categoryName }, include: { svg: true } })
.then((result: Category) => {
if (result !== null) {
res.json(result);
} else {
const error: ResponseError = {
code: "404",
message: "No category with this name found!",
};
res.status(404).json(error);
}
})
.catch((err) => {
const error: ResponseError = {
code: "500",
message: err,
};
res.status(500).json(error);
});
}

View File

@@ -1,31 +0,0 @@
import prisma from "../../lib/prisma";
export default async function handler(req, res) {
res.setHeader("Content-Type", "application/json");
let query: string = req.query?.q ?? "";
query = query.toLowerCase().replaceAll("%20", "");
query = query.toLowerCase().replaceAll(" ", "");
if (query.length > 0) {
const articles = await prisma.article.findMany({
select: { title: true, name: true },
take: 5,
}); //TODO order by most viewed
let result = [];
articles.forEach((a) => {
let title = a.title.toLowerCase().replaceAll(" ", "");
title = title.toLowerCase().replaceAll("%20", "");
if (title.includes(query)) {
result.push(a);
}
});
res.end(JSON.stringify(result));
} else {
res.end(JSON.stringify([]));
}
}

View File

@@ -0,0 +1,86 @@
import ContentTable from "@/components/ContentTable";
import Sidebar from "@/components/Sidebar";
import styles from "@/styles/modules/Article.module.scss";
import Image from "next/image";
import Markdown from "@/components/Markdown";
import { formatTextToUrlName } from "@/utils";
import prisma, { ArticleWithIncludes, CategoryWithIncludes } from "@/lib/prisma";
import articles from "../..";
//* MAIN
export default function ArticlePage({ article }: { article: ArticleWithIncludes }) {
const dateUpdated: Date = new Date(article?.dateUpdated);
const dateCreated: Date = new Date(article?.dateCreated);
const dateOptions: Intl.DateTimeFormatOptions = { month: "long", day: "numeric", year: "numeric" };
return (
<div className={styles.article}>
<ContentTable contentTableData={article?.contentTable ? article?.contentTable : []} />
<div className={styles.tutorialContent}>
<div className={styles.header}>
<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>
<div className={styles.tags}>
<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>
<Markdown value={article?.markdown ?? ""} />
</div>
<Sidebar />
</div>
);
}
export async function generateStaticParams() {
let articles: ArticleWithIncludes[] = [];
await prisma.article.findMany({ include: { category: true } }).then(
(result: ArticleWithIncludes[]) => {
if (result) {
articles = result;
}
},
(reason: any) => {
console.log(reason);
}
);
return await Promise.all(
articles.map(async (article) => ({
article: article,
}))
);
}
export async function getServerSideProps(context: any) {
const articleName = formatTextToUrlName(context.params.articleName);
let article: ArticleWithIncludes | null = null;
await prisma.article
.findUnique({
where: { name: articleName },
include: { category: true },
})
.then(
(result: ArticleWithIncludes | null) => {
if (result) {
article = JSON.parse(JSON.stringify(result));
}
},
(reason: any) => {
console.log(reason);
}
);
return {
props: { article: article }, // will be passed to the page component as props
};
}

View File

@@ -0,0 +1,80 @@
import styles from "@/styles/modules/Category.module.scss";
import Link from "next/link";
import { formatTextToUrlName } from "@/utils";
import { Article, Category } from "@prisma/client";
import prisma, { CategoryWithIncludes } from "@/lib/prisma";
export default function CategoryPage({ category }: { category: CategoryWithIncludes | null }) {
return (
<div className={styles.category}>
<h1>{category?.title}</h1>
<div className={styles.content}>
<div className={`${styles.showcase} ${styles.smallShowcase}`}>
<h2>Most popular articles</h2>
{/* {popularArticles?.map((a, i) => {
{
return (
<Link key={i} href={`/articles/${category.name}/${a.name}`}>
{a.title}
</Link>
);
}
})} */}
</div>
{/* <div className={`${styles.showcase} ${styles.smallShowcase}`}>
<h2>Most recent articles</h2>
{recentArticles?.map((a, i) => {
{
return (
<Link key={i} href={`/articles/${category.name}/${a.name}`}>
{a.name}
</Link>
);
}
})}
</div> */}
<div className={styles.showcase}>
<h2>All articles</h2>
{category?.articles
? Array.from(category?.articles).map((a: Article, i: number) => {
{
return (
<Link key={i} href={`/articles/${category.name}/${a.name}`}>
{a.title}
</Link>
);
}
})
: ""}
</div>
</div>
</div>
);
}
export async function getServerSideProps(context: any) {
const categoryName = formatTextToUrlName(context.params.categoryName);
let category: CategoryWithIncludes | null = null;
await prisma.category
.findUnique({
where: { name: categoryName },
include: { articles: true, svg: true },
})
.then(
(result: CategoryWithIncludes | null) => {
if (result) {
category = JSON.parse(JSON.stringify(result));
}
},
(reason: any) => {
console.log(reason);
}
);
return {
props: { category: category }, // will be passed to the page component as props
};
}

50
pages/articles/index.tsx Normal file
View File

@@ -0,0 +1,50 @@
import styles from "@/styles/modules/CategoryList.module.scss";
import Link from "next/link";
import { Category } from "@prisma/client";
import prisma, { CategoryWithIncludes } from "@/lib/prisma";
export default function CategoryList({ categories }: { categories: CategoryWithIncludes[] }) {
return (
<div className={styles.categoryList}>
<h1>Overview</h1>
<div className={styles.content}>
<div className={styles.grid}>
{categories?.length > 0
? categories.map((cat: CategoryWithIncludes, i: number) => {
return (
<div key={i} className={styles.linkContainer}>
<Link href={`/articles/${cat.name.toLowerCase()}`} style={{ backgroundColor: cat.color }}>
<div className={styles.svgContainer}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox={cat?.svg?.viewbox}>
<path d={cat?.svg?.path} />
</svg>
</div>
{cat.title}
</Link>
</div>
);
})
: "We did not find any categories"}
</div>
</div>
</div>
);
}
export async function getServerSideProps() {
let categories: CategoryWithIncludes[] = [];
await prisma.category.findMany({ include: { articles: true, svg: true } }).then(
(result: CategoryWithIncludes[]) => {
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
};
}

18
pages/index.tsx Normal file
View File

@@ -0,0 +1,18 @@
import Head from "next/head";
// import styles from "@/styles/Home.module.css";
export default function Home() {
return (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<h1>Home</h1>
</main>
</>
);
}