refactor
8
.gitignore
vendored
@@ -4,7 +4,7 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
/copy
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
@@ -18,9 +18,7 @@
|
|||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
|
#! .env
|
||||||
/docker_data/postgres/
|
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
@@ -36,5 +34,3 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
.vscode
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"useTabs": false,
|
"useTabs": false,
|
||||||
"printWidth": 120
|
"printWidth": 180
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ First, run the development server:
|
|||||||
npm run dev
|
npm run dev
|
||||||
# or
|
# or
|
||||||
yarn dev
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
@@ -18,6 +20,8 @@ You can start editing the page by modifying `pages/index.tsx`. The page auto-upd
|
|||||||
|
|
||||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||||
|
|
||||||
## Learn More
|
## Learn More
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export default async function AdminArticleEditorLayout({ children }) {
|
|
||||||
return <div>{children}</div>;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export default function AdminArticlesPage() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Page to manage articles</h1>
|
|
||||||
<a href="/admin/articles/editor/0">create new article</a> <br />
|
|
||||||
<p>List of existing articles</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default async function AdminCategoriesEditorLayout({ children }) {
|
|
||||||
return <div>{children}</div>;
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useRef, useState } from "react";
|
|
||||||
import styles from "../../../../../styles/modules/CategoryEditor.module.scss";
|
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
import "../../../../../styles/inputs.scss";
|
|
||||||
import "../../../../../styles/buttons.scss";
|
|
||||||
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";
|
|
||||||
|
|
||||||
type CategoryWithSvg = Prisma.CategoryGetPayload<{ include: { svg: true } }>;
|
|
||||||
|
|
||||||
export default function AdminCategoriesEditor({ params }: { params: { categoryId: string } }) {
|
|
||||||
const router = useRouter();
|
|
||||||
const [title, setTitle] = useState<string>("");
|
|
||||||
const [color, setColor] = useState<string>("");
|
|
||||||
const [svgViewbox, setSvgViewbox] = useState<string>("");
|
|
||||||
const [svgPath, setSvgPath] = useState<string>("");
|
|
||||||
|
|
||||||
const titleRef = useRef(null);
|
|
||||||
const colorRef = useRef(null);
|
|
||||||
const svgViewboxRef = useRef(null);
|
|
||||||
const svgPathRef = useRef(null);
|
|
||||||
const errorTextRef = useRef(null);
|
|
||||||
function handleFormChange() {
|
|
||||||
setTitle(titleRef.current.value);
|
|
||||||
setColor(colorRef.current.value);
|
|
||||||
setSvgPath(svgPathRef.current.value);
|
|
||||||
setSvgViewbox(svgViewboxRef.current.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleResponse(res: Response) {
|
|
||||||
const json = await res.json();
|
|
||||||
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/${params.categoryId.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchExistingCategory = async () => {
|
|
||||||
const result: Response = await fetch(urlJoin(apiUrl, `categories/${params.categoryId}`), {
|
|
||||||
cache: "no-cache",
|
|
||||||
});
|
|
||||||
|
|
||||||
const category = await result.json();
|
|
||||||
console.log(category);
|
|
||||||
if (category.code == "404") {
|
|
||||||
router.push(urlJoin(`/admin/categories/editor/0`));
|
|
||||||
} else {
|
|
||||||
titleRef.current.value = category.title;
|
|
||||||
colorRef.current.value = category.color;
|
|
||||||
svgPathRef.current.value = category.svg.path;
|
|
||||||
svgPathRef.current.value = category.svg.viewbox;
|
|
||||||
|
|
||||||
setTitle(category.title);
|
|
||||||
setColor(category.color);
|
|
||||||
setSvgPath(category.svg.path);
|
|
||||||
setSvgViewbox(category.svg.viewbox);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (params.categoryId != "0") {
|
|
||||||
fetchExistingCategory().catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.categoryEditor}>
|
|
||||||
<h1>{params.categoryId == "0" ? "Create new category" : "Update category"}</h1>
|
|
||||||
<div className={styles.formControl}>
|
|
||||||
<p className="text-error" ref={errorTextRef}></p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
if (params.categoryId != "0") {
|
|
||||||
updateCategory();
|
|
||||||
} else {
|
|
||||||
createCategory();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{params.categoryId == "0" ? "Create category" : "Update category"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className={styles.form}>
|
|
||||||
<div className={styles.title}>
|
|
||||||
<label htmlFor="title">Title</label>
|
|
||||||
<div className={styles.titleInputs}>
|
|
||||||
<input
|
|
||||||
onChange={handleFormChange}
|
|
||||||
className={!isValidText(title) && title ? "error" : ""}
|
|
||||||
type="text"
|
|
||||||
name="title"
|
|
||||||
placeholder="title"
|
|
||||||
ref={titleRef}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
readOnly={true}
|
|
||||||
onChange={handleFormChange}
|
|
||||||
className={""}
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
value={title ? formatTextToUrlName(title) : ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles.svg}>
|
|
||||||
<label>SVG</label>
|
|
||||||
<div className={styles.svgInputs}>
|
|
||||||
<input onChange={handleFormChange} type="text" placeholder="svg path" ref={svgPathRef} />
|
|
||||||
<input onChange={handleFormChange} type="text" placeholder="0 0 512 512" ref={svgViewboxRef} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.color}>
|
|
||||||
<label>Color</label>
|
|
||||||
<input onChange={handleFormChange} type="color" ref={colorRef} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
export default function AdminCategoriesPage() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Page to manage categories</h1>
|
|
||||||
<a href="/admin/categories/editor/0">create new category</a> <br />
|
|
||||||
<p>List of existing category</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
export default function AdminPage() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>AdminPage to manage explainegy</h1>
|
|
||||||
<a href="/admin/articles/">articles</a> <a href="/admin/categories/">categories</a> <br />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Article } from "@prisma/client";
|
|
||||||
import { FetchManager } from "../../../../manager/fetchManager";
|
|
||||||
|
|
||||||
export default async function ArticleHead({ params }: { params: { articleName: string; categoryName: string } }) {
|
|
||||||
const articleName: string = params.articleName;
|
|
||||||
const article: Article = await FetchManager.Article.getByName(articleName);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<title>{article?.title}</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default function ArticleLayout({ children }) {
|
|
||||||
return <div>{children}</div>;
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
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 { ArticleWithIncludes, FetchManager } from "../../../../manager/fetchManager";
|
|
||||||
import { formatTextToUrlName } from "../../../../utils";
|
|
||||||
|
|
||||||
//* MAIN
|
|
||||||
export default async function ArticlePage({
|
|
||||||
params,
|
|
||||||
}: {
|
|
||||||
params: { articleName: string; categoryName: string; test: string };
|
|
||||||
}) {
|
|
||||||
const articleName: string = formatTextToUrlName(params.articleName);
|
|
||||||
const article: ArticleWithIncludes = await FetchManager.Article.getByName(articleName);
|
|
||||||
|
|
||||||
const dateUpdated: Date = new Date(article.dateUpdated);
|
|
||||||
const dateCreated: Date = new Date(article.dateCreated);
|
|
||||||
const dateOptions: Intl.DateTimeFormatOptions = { month: "long", day: "numeric", year: "numeric" };
|
|
||||||
const markdown: string = article?.markdown ?? "";
|
|
||||||
|
|
||||||
console.log(params.test);
|
|
||||||
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={article?.image?.url ?? ""}
|
|
||||||
height={350}
|
|
||||||
width={750}
|
|
||||||
alt={article?.image?.alt ?? ""}
|
|
||||||
quality={100}
|
|
||||||
placeholder="blur"
|
|
||||||
blurDataURL="/images/blur.png"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<p>{article?.introduction}</p>
|
|
||||||
</div>
|
|
||||||
<Markdown value={markdown} />
|
|
||||||
</div>
|
|
||||||
<Sidebar />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
|
||||||
// Fetchmanager does not work here
|
|
||||||
const articles: ArticleWithIncludes[] = await FetchManager.Article.list(false);
|
|
||||||
|
|
||||||
return await Promise.all(
|
|
||||||
articles.map(async (article) => ({
|
|
||||||
categoryName: article.category?.name ?? "",
|
|
||||||
articleName: article.name ?? "",
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getServerSideProps() {
|
|
||||||
console.log("-----------------------------------");
|
|
||||||
return { test: "weird test" };
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export default async function RootHead() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import "../styles/globals.scss";
|
|
||||||
import "../styles/variables_colors.scss";
|
|
||||||
import "../styles/variables.scss";
|
|
||||||
import Link from "next/link";
|
|
||||||
import Footer from "../components/Footer";
|
|
||||||
import Nav from "../components/Nav";
|
|
||||||
import { FetchManager } from "../manager/fetchManager";
|
|
||||||
import AdminNav from "../components/AdminNav";
|
|
||||||
import AdminControl from "../components/AdminControl";
|
|
||||||
|
|
||||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<html style={{ scrollBehavior: "smooth" }}>
|
|
||||||
<head></head>
|
|
||||||
|
|
||||||
<body className="body">
|
|
||||||
<header>
|
|
||||||
<Nav categories={await FetchManager.Category.list()} />
|
|
||||||
<AdminNav />
|
|
||||||
<AdminControl />
|
|
||||||
</header>
|
|
||||||
<main>{children}</main>
|
|
||||||
<Footer />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default function HomePage() {
|
|
||||||
return <h1>Home</h1>;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
export default function TypograhyPage() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Testing this headline</h1>
|
|
||||||
<h2>Testing this headline</h2>
|
|
||||||
<h3>Testing this headline</h3>
|
|
||||||
<h4>Testing this headline</h4>
|
|
||||||
<h5>Testing this headline</h5>
|
|
||||||
<h6>Testing this headline</h6>
|
|
||||||
<br />
|
|
||||||
<p>
|
|
||||||
This is a paragraph Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolores enim unde obcaecati ea
|
|
||||||
harum voluptate, nisi quia. Quod, et autem! Aperiam mollitia ullam ab eaque quidem facilis est ducimus delectus.
|
|
||||||
</p>
|
|
||||||
<br />
|
|
||||||
<a href="#">This is a link</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import styles from "../styles/modules/AdminControl.module.scss";
|
|
||||||
import "../styles/buttons.scss";
|
|
||||||
|
|
||||||
export default function AdminControl() {
|
|
||||||
const router = useRouter();
|
|
||||||
const [isArticle, setIsArticle] = useState(false);
|
|
||||||
const [isCategory, setIsCategory] = useState(false);
|
|
||||||
const [articleOrCategoryName, setArticleOrCategoryName] = useState("");
|
|
||||||
const [articleOrCategoryId, setArticleOrCategoryId] = useState();
|
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
async function fetchDelete() {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/${isArticle ? "articles" : isCategory ? "categories" : ""}/${articleOrCategoryId}`,
|
|
||||||
{
|
|
||||||
method: "DELETE",
|
|
||||||
cache: "no-cache",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
router.push("/articles/");
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function checkArticlePage() {
|
|
||||||
let path = pathname.split("/");
|
|
||||||
if (path.length == 4) {
|
|
||||||
if (path[1] == "articles") {
|
|
||||||
setIsArticle(true);
|
|
||||||
setArticleOrCategoryName(path[3]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsArticle(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkCategoryPage() {
|
|
||||||
let path = pathname.split("/");
|
|
||||||
if (path.length == 3) {
|
|
||||||
if (path[1] == "articles") {
|
|
||||||
setIsCategory(true);
|
|
||||||
setArticleOrCategoryName(path[2]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsCategory(false);
|
|
||||||
}
|
|
||||||
checkArticlePage();
|
|
||||||
checkCategoryPage();
|
|
||||||
}, [pathname]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getArticleOrCategoryId() {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/${isArticle ? "articles" : isCategory ? "categories" : ""}/name/${articleOrCategoryName}`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
cache: "no-cache",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const json = await response.json();
|
|
||||||
|
|
||||||
setArticleOrCategoryId(json.id);
|
|
||||||
}
|
|
||||||
getArticleOrCategoryId();
|
|
||||||
}, [articleOrCategoryName]);
|
|
||||||
return (
|
|
||||||
<div className={styles.adminControl}>
|
|
||||||
{isArticle || isCategory ? (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="danger"
|
|
||||||
onClick={() => {
|
|
||||||
fetchDelete();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete this {isArticle ? "article" : "category"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="warning"
|
|
||||||
onClick={() => {
|
|
||||||
router.push(`/admin/${isArticle ? "articles" : "categories"}/editor/${articleOrCategoryId}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Edit this {isArticle ? "article" : "category"}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import styles from "../styles/modules/AdminNav.module.scss";
|
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"}>Admin</Link>
|
||||||
<Link href={"/admin/articles/editor/0"}>New article</Link>
|
<Link href={"/admin/editor/article/0"}>New article</Link>
|
||||||
<Link href={"/admin/categories/editor/0"}>New category</Link>
|
<Link href={"/admin/editor/category/0"}>New category</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
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";
|
||||||
|
|
||||||
export default function ContentTable({ contentTableData }: { contentTableData: any }) {
|
export default function ContentTable({
|
||||||
|
contentTableData,
|
||||||
|
}: {
|
||||||
|
contentTableData: any;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.articleContentTable}>
|
<div className={styles.articleContentTable}>
|
||||||
<div className={styles.stickyContainer}>
|
<div className={styles.stickyContainer}>
|
||||||
<div className={styles.list}>
|
<div className={styles.list}>
|
||||||
<h2>Contents</h2>
|
<h2>Contents</h2>
|
||||||
{contentTableData?.map((e, i) => {
|
{contentTableData?.map((e: IContentTableEntry, i: number) => {
|
||||||
return (
|
return (
|
||||||
<a key={i} href={"#" + e.anchor}>
|
<a key={i} href={"#" + e.anchor}>
|
||||||
{e.title}
|
{e.title}
|
||||||
@@ -15,7 +20,11 @@ export default function ContentTable({ contentTableData }: { contentTableData: a
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{contentTableData?.length < 15 ? <div className={styles.adContainer}>Future advertisement</div> : ""}
|
{contentTableData?.length < 15 ? (
|
||||||
|
<div className={styles.adContainer}>Future advertisement</div>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import styles from "../styles/modules/Footer.module.scss";
|
import styles from "@/styles/modules/Footer.module.scss";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Gallery as ReactGridGallery, Image as ImageType, ThumbnailImageProps } from "react-grid-gallery";
|
|
||||||
import Image from "next/image";
|
|
||||||
const ImageComponent = (props: ThumbnailImageProps) => {
|
|
||||||
const { src, alt, style, title } = props.imageProps;
|
|
||||||
const { width, height } = props.item;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
alt={alt}
|
|
||||||
src={src}
|
|
||||||
title={title || ""}
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
onClick={() => {
|
|
||||||
window.open(src);
|
|
||||||
}}
|
|
||||||
style={style}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Gallery({ images }: { images: ImageType[] }) {
|
|
||||||
return <ReactGridGallery images={images} enableImageSelection={false} thumbnailImageComponent={ImageComponent} />;
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
|
||||||
"use client";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { useRef } from "react";
|
|
||||||
export default function ImageUpload() {
|
|
||||||
const [selectedImage, setSelectedImage] = useState(null);
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const handleImageChange = (event) => {
|
|
||||||
setSelectedImage(URL.createObjectURL(event.target.files[0]));
|
|
||||||
};
|
|
||||||
|
|
||||||
async function uploadImage() {
|
|
||||||
if (selectedImage) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("image", selectedImage);
|
|
||||||
const response = await fetch("/api/images/", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
console.log(await response.json());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input onChange={handleImageChange} ref={inputRef} type="file" name="image" accept="image/*" />
|
|
||||||
{selectedImage && <img src={selectedImage} alt="Selected" />}
|
|
||||||
<button onClick={uploadImage}>Upload</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
import oneDark from "react-syntax-highlighter/dist/esm/styles/prism/one-dark";
|
import oneDark from "react-syntax-highlighter/dist/cjs/styles/prism/one-dark";
|
||||||
import oneLight from "react-syntax-highlighter/dist/esm/styles/prism/one-light";
|
import oneLight from "react-syntax-highlighter/dist/cjs/styles/prism/one-light";
|
||||||
import styles from "../styles/modules/markdown.module.scss";
|
import styles from "../styles/modules/markdown.module.scss";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import remarkGemoji from "remark-gemoji";
|
import remarkGemoji from "remark-gemoji";
|
||||||
@@ -13,11 +13,11 @@ import React from "react";
|
|||||||
|
|
||||||
import { formatTextToUrlName } from "../utils";
|
import { formatTextToUrlName } from "../utils";
|
||||||
|
|
||||||
function flatten(text, child) {
|
function flatten(text: string, child: any): any {
|
||||||
return typeof child === "string" ? text + child : React.Children.toArray(child.props.children).reduce(flatten, text);
|
return typeof child === "string" ? text + child : React.Children.toArray(child.props.children).reduce(flatten, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function HeadingRenderer({ children, level }) {
|
function HeadingRenderer({ children, level }: { children: any; level: any }) {
|
||||||
children = React.Children.toArray(children);
|
children = React.Children.toArray(children);
|
||||||
const text = children.reduce(flatten, "");
|
const text = children.reduce(flatten, "");
|
||||||
return React.createElement("h" + level, { id: formatTextToUrlName(text) }, children);
|
return React.createElement("h" + level, { id: formatTextToUrlName(text) }, children);
|
||||||
@@ -55,7 +55,13 @@ export default function Markdown({ value }: { value: any }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SyntaxHighlighter style={oneDark} language={match ? match[1] : ""} PreTag="div" {...props}>
|
<SyntaxHighlighter
|
||||||
|
// @ts-ignore
|
||||||
|
style={oneDark}
|
||||||
|
language={match ? match[1] : ""}
|
||||||
|
PreTag="div"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{String(children).replace(/\n$/, "")}
|
{String(children).replace(/\n$/, "")}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
"use client";
|
import styles from "@/styles/modules/Nav.module.scss";
|
||||||
import styles from "../styles/modules/Nav.module.scss";
|
|
||||||
import Image from "next/image";
|
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";
|
||||||
|
|
||||||
function switchTheme(theme) {
|
function switchTheme(theme: string) {
|
||||||
const bodyElement = document.getElementsByTagName("body")[0];
|
const bodyElement = document.getElementsByTagName("body")[0];
|
||||||
|
|
||||||
if (theme == "dark") {
|
if (theme == "dark") {
|
||||||
@@ -18,6 +17,7 @@ function switchTheme(theme) {
|
|||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
const svgElement = document.getElementById("themeSwitchSvg");
|
const svgElement = document.getElementById("themeSwitchSvg");
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -37,11 +37,14 @@ function toggleTheme() {
|
|||||||
svgElement.style.animationName = "";
|
svgElement.style.animationName = "";
|
||||||
}, 150);
|
}, 150);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function Nav({ categories }: { categories: Category[] }) {
|
export default function Nav({ categories }: { categories: Category[] }) {
|
||||||
const [searchResults, setSearchResults] = useState([]);
|
const [searchResults, setSearchResults] = useState<
|
||||||
|
{ name: string; title: string }[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
async function handleSearchInput(event) {
|
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();
|
||||||
@@ -88,7 +91,10 @@ export default function Nav({ categories }: { categories: Category[] }) {
|
|||||||
{categories?.map((cat, i) => {
|
{categories?.map((cat, i) => {
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Link key={i} href={`/articles/${cat.name.toLowerCase()}`}>
|
<Link
|
||||||
|
key={i}
|
||||||
|
href={`/articles/${cat.name.toLowerCase()}`}
|
||||||
|
>
|
||||||
{cat.title}
|
{cat.title}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -112,7 +118,11 @@ export default function Nav({ categories }: { categories: Category[] }) {
|
|||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{searchResults.map((s) => {
|
{searchResults.map((s) => {
|
||||||
{
|
{
|
||||||
return <Link href={`/articles/${s.name.toLowerCase()}`}>{s.title}</Link>;
|
return (
|
||||||
|
<Link href={`/articles/${s.name.toLowerCase()}`}>
|
||||||
|
{s.title}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -125,7 +135,11 @@ export default function Nav({ categories }: { categories: Category[] }) {
|
|||||||
toggleTheme();
|
toggleTheme();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg id="themeSwitchSvg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import styles from "../styles/modules/Sidebar.module.scss";
|
import styles from "@/styles/modules/Sidebar.module.scss";
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
return (
|
return (
|
||||||
<div className={styles.sidebar}>
|
<div className={styles.sidebar}>
|
||||||
|
|||||||
@@ -1,14 +1,32 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
|
import { Prisma, PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace NodeJS {
|
||||||
|
interface Global {
|
||||||
|
prisma: PrismaClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let prisma: PrismaClient;
|
let prisma: PrismaClient;
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (typeof window === "undefined") {
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
prisma = new PrismaClient();
|
prisma = new PrismaClient();
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
if (!global.prisma) {
|
if (!global.prisma) {
|
||||||
|
// @ts-ignore
|
||||||
global.prisma = new PrismaClient();
|
global.prisma = new PrismaClient();
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
prisma = global.prisma;
|
prisma = global.prisma;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
export default prisma;
|
export default prisma;
|
||||||
|
|
||||||
|
export type CategoryWithIncludes = Prisma.CategoryGetPayload<{ include: { articles: true, svg: true } }>
|
||||||
|
export type ArticleWithIncludes = Prisma.ArticleGetPayload<{ include: { category: true } }>
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
import { Article, Category } from '@prisma/client';
|
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
import urlJoin from "url-join";
|
|
||||||
import { apiUrl } from "../global";
|
|
||||||
|
|
||||||
const GLOBAL_NO_CACHE: boolean = true;
|
|
||||||
export type ArticleWithIncludes = Prisma.ArticleGetPayload<{ include: { category: true, image: true } }>
|
|
||||||
export type CategoryWithIncludes = Prisma.CategoryGetPayload<{ include: { svg: true } }>
|
|
||||||
|
|
||||||
export interface FetchError {
|
|
||||||
code: number;
|
|
||||||
message?: string;
|
|
||||||
data?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FetchManager {
|
|
||||||
|
|
||||||
|
|
||||||
static Article = class {
|
|
||||||
static async list(noCache: boolean = false): Promise<ArticleWithIncludes[]> {
|
|
||||||
console.log(urlJoin(apiUrl, `articles`))
|
|
||||||
const response = await fetch(urlJoin(apiUrl, `articles`), {
|
|
||||||
cache: GLOBAL_NO_CACHE || noCache ? "no-cache" : "force-cache",
|
|
||||||
next: { revalidate: 60 * 10 },
|
|
||||||
})
|
|
||||||
return await response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
static async get(id: string, noCache: boolean = false): Promise<ArticleWithIncludes> {
|
|
||||||
urlJoin(apiUrl, `articles/${id}`)
|
|
||||||
const response = await fetch(urlJoin(apiUrl, `articles/${id}`), {
|
|
||||||
cache: GLOBAL_NO_CACHE || noCache ? "no-cache" : "force-cache",
|
|
||||||
next: { revalidate: 60 * 10 },
|
|
||||||
})
|
|
||||||
|
|
||||||
return await response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getByName(name: string, noCache: boolean = false): Promise<ArticleWithIncludes> {
|
|
||||||
|
|
||||||
const response = await fetch(urlJoin(apiUrl, `articles/name/${name}`), {
|
|
||||||
cache: GLOBAL_NO_CACHE || noCache ? "no-cache" : "force-cache",
|
|
||||||
next: { revalidate: 60 * 10 },
|
|
||||||
})
|
|
||||||
return await response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getByCategory(name: string, noCache: boolean = false): Promise<ArticleWithIncludes[]> {
|
|
||||||
|
|
||||||
const response = await fetch(urlJoin(apiUrl, `articles?categoryName=${name}`), {
|
|
||||||
cache: GLOBAL_NO_CACHE || noCache ? "no-cache" : "force-cache",
|
|
||||||
next: { revalidate: 60 * 10 },
|
|
||||||
})
|
|
||||||
return await response.json()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static Category = class {
|
|
||||||
static async list(noCache: boolean = false): Promise<CategoryWithIncludes[]> {
|
|
||||||
const response = await fetch(urlJoin(apiUrl, `categories`), {
|
|
||||||
cache: GLOBAL_NO_CACHE || noCache ? "no-cache" : "force-cache",
|
|
||||||
next: { revalidate: 60 * 10 },
|
|
||||||
})
|
|
||||||
return await response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
static async get(id: string, noCache: boolean = false): Promise<CategoryWithIncludes> {
|
|
||||||
const response = await fetch(urlJoin(apiUrl, `categories/${id}`), {
|
|
||||||
cache: GLOBAL_NO_CACHE || noCache ? "no-cache" : "force-cache",
|
|
||||||
next: { revalidate: 60 * 10 },
|
|
||||||
})
|
|
||||||
|
|
||||||
return await response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getByName(name: string, noCache: boolean = false): Promise<CategoryWithIncludes> {
|
|
||||||
|
|
||||||
const response = await fetch(urlJoin(apiUrl, `categories/name/${name}`), {
|
|
||||||
cache: GLOBAL_NO_CACHE || noCache ? "no-cache" : "force-cache",
|
|
||||||
next: { revalidate: 60 * 10 },
|
|
||||||
})
|
|
||||||
return await response.json()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
experimental: {
|
reactStrictMode: true,
|
||||||
appDir: true,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = nextConfig;
|
module.exports = nextConfig;
|
||||||
|
|||||||
7827
package-lock.json
generated
49
package.json
@@ -1,55 +1,38 @@
|
|||||||
{
|
{
|
||||||
"name": "explainegy",
|
"name": "explainegy_pages",
|
||||||
"version": "0.2.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prisma": "prisma generate && prisma db push && prisma studio",
|
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint"
|
||||||
"vercel-build": "prisma generate && prisma db push && next build",
|
|
||||||
"prisma:generate": "prisma generate"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/font": "13.0.7",
|
"@next/font": "13.1.6",
|
||||||
"@prisma/client": "^4.9.0",
|
"@prisma/client": "^4.9.0",
|
||||||
"@types/express": "^4.17.15",
|
"@types/node": "18.11.19",
|
||||||
"@types/formidable": "^2.0.5",
|
"@types/react": "18.0.27",
|
||||||
"@types/marked": "^4.0.8",
|
"@types/react-dom": "18.0.10",
|
||||||
"@types/react": "18.0.26",
|
"eslint": "8.33.0",
|
||||||
"@types/react-dom": "18.0.9",
|
"eslint-config-next": "13.1.6",
|
||||||
"encoding": "^0.1.13",
|
"next": "13.1.6",
|
||||||
"eslint": "8.30.0",
|
"next-superjson-plugin": "^0.5.4",
|
||||||
"eslint-config-next": "13.0.7",
|
|
||||||
"formidable": "^2.1.1",
|
|
||||||
"marked": "^4.2.4",
|
|
||||||
"multer": "^1.4.5-lts.1",
|
|
||||||
"next": "^13.1.6",
|
|
||||||
"prismjs": "^1.29.0",
|
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-code-blocks": "^0.0.9-0",
|
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-grid-gallery": "^1.0.0",
|
"react-markdown": "^8.0.5",
|
||||||
"react-markdown": "^8.0.4",
|
|
||||||
"react-select": "^5.7.0",
|
"react-select": "^5.7.0",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
|
||||||
"remark-code-blocks": "^2.0.1",
|
|
||||||
"remark-codesandbox": "^0.10.1",
|
|
||||||
"remark-emoji": "^3.0.2",
|
|
||||||
"remark-gemoji": "^7.0.1",
|
"remark-gemoji": "^7.0.1",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-stringify": "^10.0.2",
|
"remark-stringify": "^10.0.2",
|
||||||
"sass": "^1.57.0",
|
"sass": "^1.58.0",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.5",
|
||||||
"url-join": "^5.0.0"
|
"url-join": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fec/remark-a11y-emoji": "^3.1.0",
|
"@types/react-select": "^5.0.1",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/react-syntax-highlighter": "^15.5.6",
|
||||||
"@types/node": "^18.11.17",
|
|
||||||
"@types/prismjs": "^1.26.0",
|
|
||||||
"prisma": "^4.9.0"
|
"prisma": "^4.9.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
pages/_app.tsx
Normal 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
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,40 +1,40 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import styles from "../../../../../styles/modules/ArticleEditor.module.scss";
|
import styles from "@/styles/modules/ArticleEditor.module.scss";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma, Category } from "@prisma/client";
|
||||||
import "../../../../../styles/inputs.scss";
|
|
||||||
import "../../../../../styles/buttons.scss";
|
|
||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import urlJoin from "url-join";
|
import urlJoin from "url-join";
|
||||||
import { IContentTableEntry } from "../../../../../types/contentTable";
|
import { IContentTableEntry } from "@/types/contentTable";
|
||||||
import { CreateArticle, UpdateArticle } from "../../../../../types/api";
|
import { CreateArticle, UpdateArticle } from "@/types/api";
|
||||||
import { formatTextToUrlName } from "../../../../../utils";
|
import { formatTextToUrlName } from "@/utils";
|
||||||
import { isValidText } from "../../../../../validators";
|
import { isValidText } from "@/validators";
|
||||||
import { apiUrl } from "../../../../../global";
|
import { apiUrl } from "@/global";
|
||||||
import Markdown from "../../../../../components/Markdown";
|
import Markdown from "@/components/Markdown";
|
||||||
|
import prisma, { ArticleWithIncludes, CategoryWithIncludes } from "@/lib/prisma";
|
||||||
|
import { CLIENT_RENEG_LIMIT } from "tls";
|
||||||
|
|
||||||
type ArticleWithCategory = Prisma.ArticleGetPayload<{ include: { category: true } }>;
|
export default function AdminArticlesEditorPage({ article, categories }: { article: ArticleWithIncludes | null; categories: CategoryWithIncludes[] }) {
|
||||||
|
|
||||||
export default function AdminArticlesEditorPage({ params }: { params: { articleId: string } }) {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [title, setTitle] = useState<string>("");
|
|
||||||
const [selectCategoriesOptions, setSelectCategoriesOptions] = useState<any>([]);
|
const [title, setTitle] = useState<string>(article?.title ?? "");
|
||||||
const [introduction, setIntroduction] = useState<string>("");
|
const [selectCategoriesOptions, setSelectCategoriesOptions] = useState<{ value: string; label: string }[]>(
|
||||||
const [markdown, setMarkdown] = useState<string>("");
|
categories?.map((c: CategoryWithIncludes) => ({ value: c.id, label: c.title }))
|
||||||
const [contentTable, setContentTable] = useState<any>([]);
|
);
|
||||||
|
|
||||||
|
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 titleRef = useRef<HTMLInputElement>(null);
|
||||||
const categorySelectRef = useRef(null);
|
const categorySelectRef = useRef<any>(null);
|
||||||
const introductionRef = useRef<HTMLInputElement>(null);
|
const introductionRef = useRef<HTMLInputElement>(null);
|
||||||
const markdownTextAreaRef = useRef<HTMLTextAreaElement>(null);
|
const markdownTextAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
const errorTextRef = useRef(null);
|
const errorTextRef = useRef<HTMLParagraphElement>(null);
|
||||||
|
|
||||||
function changeContentTableEntryAnchor(index: number, newAnchor: string) {
|
function changeContentTableEntryAnchor(index: number, newAnchor: string) {
|
||||||
setContentTable((prevArray) => {
|
setContentTable((prevArray: any) => {
|
||||||
let newArray = [...prevArray];
|
let newArray = [...prevArray];
|
||||||
newArray[index].anchor = newAnchor;
|
newArray[index].anchor = newAnchor;
|
||||||
return newArray;
|
return newArray;
|
||||||
@@ -42,7 +42,7 @@ export default function AdminArticlesEditorPage({ params }: { params: { articleI
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changeContentTableEntryTitle(index: number, newTitle: string) {
|
function changeContentTableEntryTitle(index: number, newTitle: string) {
|
||||||
setContentTable((prevArray) => {
|
setContentTable((prevArray: any) => {
|
||||||
let newArray = [...prevArray];
|
let newArray = [...prevArray];
|
||||||
newArray[index].anchor = newTitle;
|
newArray[index].anchor = newTitle;
|
||||||
return newArray;
|
return newArray;
|
||||||
@@ -55,18 +55,15 @@ export default function AdminArticlesEditorPage({ params }: { params: { articleI
|
|||||||
setContentTable(newArray);
|
setContentTable(newArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFormChange() {
|
|
||||||
setMarkdown(markdownTextAreaRef.current.value);
|
|
||||||
setTitle(titleRef.current.value);
|
|
||||||
setIntroduction(introductionRef.current.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create or update article
|
// Create or update article
|
||||||
async function handleResponse(res: Response) {
|
async function handleResponse(res: Response) {
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
if (errorTextRef?.current) {
|
||||||
errorTextRef.current.innerText = json.error ?? "";
|
errorTextRef.current.innerText = json.error ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
if (json.success) {
|
if (json.success) {
|
||||||
const newArticle: ArticleWithCategory = json.data;
|
const newArticle: ArticleWithIncludes = json.data;
|
||||||
router.push(urlJoin(`/articles/`, newArticle.category.name, newArticle.name));
|
router.push(urlJoin(`/articles/`, newArticle.category.name, newArticle.name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,15 +71,15 @@ export default function AdminArticlesEditorPage({ params }: { params: { articleI
|
|||||||
async function updateArticle() {
|
async function updateArticle() {
|
||||||
console.log("Update article");
|
console.log("Update article");
|
||||||
const payload: UpdateArticle = {
|
const payload: UpdateArticle = {
|
||||||
title: titleRef.current.value,
|
title: titleRef?.current?.value,
|
||||||
introduction: introductionRef.current.value,
|
introduction: introductionRef?.current?.value,
|
||||||
markdown: markdown,
|
markdown: markdown,
|
||||||
categoryId: Number(categorySelectRef?.current?.getValue()[0]?.value),
|
categoryId: categorySelectRef?.current?.getValue()[0]?.value,
|
||||||
contentTable: contentTable,
|
contentTable: contentTable,
|
||||||
};
|
};
|
||||||
console.log(payload);
|
console.log(payload);
|
||||||
|
|
||||||
await fetch(`/api/articles/${params.articleId.toString()}`, {
|
await fetch(`/api/articles/${article?.id.toString()}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
@@ -98,10 +95,10 @@ export default function AdminArticlesEditorPage({ params }: { params: { articleI
|
|||||||
async function createArticle() {
|
async function createArticle() {
|
||||||
console.log("Create article");
|
console.log("Create article");
|
||||||
const payload: CreateArticle = {
|
const payload: CreateArticle = {
|
||||||
title: titleRef.current.value,
|
title: titleRef?.current?.value ?? "",
|
||||||
introduction: introductionRef.current.value,
|
introduction: introductionRef?.current?.value ?? "",
|
||||||
markdown: markdown,
|
markdown: markdown,
|
||||||
categoryId: Number(categorySelectRef?.current?.getValue()[0]?.value),
|
categoryId: categorySelectRef?.current?.getValue()[0]?.value,
|
||||||
contentTable: contentTable,
|
contentTable: contentTable,
|
||||||
};
|
};
|
||||||
console.log(payload);
|
console.log(payload);
|
||||||
@@ -119,73 +116,22 @@ export default function AdminArticlesEditorPage({ params }: { params: { articleI
|
|||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// App
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchExistingArticle = async () => {
|
|
||||||
const result: Response = await fetch(urlJoin(apiUrl, `articles/${params.articleId}`), {
|
|
||||||
cache: "no-cache",
|
|
||||||
next: { revalidate: 60 * 1 },
|
|
||||||
});
|
|
||||||
|
|
||||||
const article = await result.json();
|
|
||||||
console.log(article);
|
|
||||||
if (article.code == "404") {
|
|
||||||
router.push(urlJoin(`/admin/articles/editor/0`));
|
|
||||||
} else {
|
|
||||||
titleRef.current.value = article.title;
|
|
||||||
introductionRef.current.value = article.introduction;
|
|
||||||
markdownTextAreaRef.current.value = article.markdown;
|
|
||||||
categorySelectRef.current.setValue({ value: article.category.id, label: article.category.title });
|
|
||||||
|
|
||||||
setTitle(article.title);
|
|
||||||
setIntroduction(article.introduction);
|
|
||||||
setMarkdown(article.markdown);
|
|
||||||
setContentTable(article.contentTable);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchCategoryOptions = async () => {
|
|
||||||
const result: Response = await fetch(urlJoin(apiUrl, `categories`), {
|
|
||||||
cache: "no-cache",
|
|
||||||
next: { revalidate: 60 * 1 },
|
|
||||||
});
|
|
||||||
|
|
||||||
const categories = await result.json();
|
|
||||||
let newSelectCategoriesOptions = [];
|
|
||||||
|
|
||||||
categories?.forEach((c) => {
|
|
||||||
newSelectCategoriesOptions.push({ value: c.id, label: c.title });
|
|
||||||
});
|
|
||||||
setSelectCategoriesOptions(newSelectCategoriesOptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchCategoryOptions().catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (params.articleId != "0") {
|
|
||||||
fetchExistingArticle().catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.adminArticlesCreate}>
|
<div className={styles.adminArticlesCreate}>
|
||||||
<h1>{params.articleId == "0" ? "Create new article" : "Update article"}</h1>
|
<h1>{article ? "Update article" : "Create new article"}</h1>
|
||||||
<div className={styles.formControl}>
|
<div className={styles.formControl}>
|
||||||
<p className="text-error" ref={errorTextRef}></p>
|
<p className="text-error" ref={errorTextRef}></p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (params.articleId != "0") {
|
if (article) {
|
||||||
updateArticle();
|
updateArticle();
|
||||||
} else {
|
} else {
|
||||||
createArticle();
|
createArticle();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{params.articleId == "0" ? "Create article" : "Update article"}
|
{article ? "Update article" : "Create article"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -196,21 +142,17 @@ export default function AdminArticlesEditorPage({ params }: { params: { articleI
|
|||||||
|
|
||||||
<div className={styles.titleInputs}>
|
<div className={styles.titleInputs}>
|
||||||
<input
|
<input
|
||||||
onChange={handleFormChange}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setTitle(e.target.value);
|
||||||
|
}}
|
||||||
className={!isValidText(title) && title ? "error" : ""}
|
className={!isValidText(title) && title ? "error" : ""}
|
||||||
type="text"
|
type="text"
|
||||||
name="title"
|
name="title"
|
||||||
placeholder="title"
|
placeholder="title"
|
||||||
ref={titleRef}
|
ref={titleRef}
|
||||||
|
defaultValue={title}
|
||||||
/>
|
/>
|
||||||
<input
|
<input readOnly={true} className={""} type="text" name="name" value={title ? formatTextToUrlName(title) : ""} />
|
||||||
readOnly={true}
|
|
||||||
onChange={handleFormChange}
|
|
||||||
className={""}
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
value={title ? formatTextToUrlName(title) : ""}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.category}>
|
<div className={styles.category}>
|
||||||
@@ -219,25 +161,34 @@ export default function AdminArticlesEditorPage({ params }: { params: { articleI
|
|||||||
ref={categorySelectRef}
|
ref={categorySelectRef}
|
||||||
className="react-select-container"
|
className="react-select-container"
|
||||||
classNamePrefix="react-select"
|
classNamePrefix="react-select"
|
||||||
onChange={handleFormChange}
|
|
||||||
options={selectCategoriesOptions}
|
options={selectCategoriesOptions}
|
||||||
|
defaultValue={article ? { value: article.category.id, label: article.category.title } : {}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.introduction}>
|
<div className={styles.introduction}>
|
||||||
<label htmlFor="title">Introduction</label>
|
<label htmlFor="title">Introduction</label>
|
||||||
<input
|
<input
|
||||||
onChange={handleFormChange}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setIntroduction(e.target.value);
|
||||||
|
}}
|
||||||
className={!isValidText(introduction) && introduction ? "error" : ""}
|
className={!isValidText(introduction) && introduction ? "error" : ""}
|
||||||
type="text"
|
type="text"
|
||||||
name="introduction"
|
name="introduction"
|
||||||
placeholder="Introduction"
|
placeholder="Introduction"
|
||||||
ref={introductionRef}
|
ref={introductionRef}
|
||||||
|
defaultValue={introduction}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.markdown}>
|
<div className={styles.markdown}>
|
||||||
<label htmlFor="">Markdown Editor</label>
|
<label htmlFor="">Markdown Editor</label>
|
||||||
<div className={styles.markdownEditor}>
|
<div className={styles.markdownEditor}>
|
||||||
<textarea ref={markdownTextAreaRef} onChange={handleFormChange}></textarea>
|
<textarea
|
||||||
|
ref={markdownTextAreaRef}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setMarkdown(e.target.value);
|
||||||
|
}}
|
||||||
|
defaultValue={markdown}
|
||||||
|
></textarea>
|
||||||
<Markdown value={markdown} />
|
<Markdown value={markdown} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -292,3 +243,43 @@ export default function AdminArticlesEditorPage({ params }: { params: { articleI
|
|||||||
</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
|
||||||
|
};
|
||||||
|
}
|
||||||
186
pages/admin/editor/category/[categoryId].tsx
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,64 +10,33 @@ import { isValidText } from "../../../validators";
|
|||||||
type ArticleWithIncludes = Prisma.ArticleGetPayload<{ include: { contentTableEntries: true, category: true, image: true } }>
|
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
|
||||||
|
console.log("PUT")
|
||||||
|
const articleId: string = formatTextToUrlName(req.query?.articleId?.toString() ?? "")
|
||||||
|
|
||||||
console.log(`API articleId: ${articleId}`)
|
console.log(`API articleId: ${articleId}`)
|
||||||
if (req.method == "GET") { //* GET
|
const articleData: UpdateArticle = req.body;
|
||||||
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
|
|
||||||
console.log("PUT")
|
|
||||||
const data: UpdateArticle = req.body;
|
|
||||||
|
|
||||||
if (!isValidText(data.title)) {
|
if (articleData.title && !isValidText(articleData.title)) {
|
||||||
res.json({ target: "title", error: "Not a valid title" });
|
res.json({ target: "title", error: "Not a valid title" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidText(data.introduction)) {
|
if (articleData.introduction && !isValidText(articleData.introduction)) {
|
||||||
res.json({ target: "introduction", error: "Not a valid introduction" });
|
res.json({ target: "introduction", error: "Not a valid introduction" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.categoryId) {
|
|
||||||
res.json({ target: "category", error: "Category is required" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newArticle: Prisma.ArticleUncheckedUpdateInput = {
|
const newArticle: Prisma.ArticleUncheckedUpdateInput = {
|
||||||
title: data.title ?? undefined,
|
title: articleData.title ?? undefined,
|
||||||
name: formatTextToUrlName(data.title) ?? undefined,
|
name: articleData.title ? formatTextToUrlName(articleData.title) : undefined,
|
||||||
introduction: data.introduction ?? undefined,
|
introduction: articleData.introduction ?? undefined,
|
||||||
|
|
||||||
categoryId: data.categoryId?.toString() ?? undefined,
|
categoryId: articleData.categoryId ?? undefined,
|
||||||
contentTable: data.contentTable ?? undefined,
|
contentTable: articleData.contentTable ?? undefined,
|
||||||
markdown: data.markdown ?? undefined,
|
markdown: articleData.markdown ?? undefined,
|
||||||
imageId: data.imageId?.toString() ?? undefined,
|
imageUrl: articleData.imageUrl ?? undefined,
|
||||||
}
|
}
|
||||||
console.log(newArticle)
|
console.log(newArticle)
|
||||||
await prisma.article.update({ data: newArticle, where: { id: articleId }, include: { category: true } })
|
await prisma.article.update({ data: newArticle, where: { id: articleId }, include: { category: true } })
|
||||||
|
|||||||
@@ -1,92 +1,48 @@
|
|||||||
import prisma from "../../../lib/prisma";
|
import { CreateArticle } from "@/types/api";
|
||||||
import { Prisma } from "@prisma/client";
|
import prisma, { ArticleWithIncludes } from "../../../lib/prisma";
|
||||||
import { Article, Category } from "@prisma/client";
|
|
||||||
import { ResponseError } from "../../../types/responseErrors";
|
|
||||||
import { formatTextToUrlName } from "../../../utils";
|
import { formatTextToUrlName } from "../../../utils";
|
||||||
import { isValidText } from "../../../validators";
|
import { isValidText } from "../../../validators";
|
||||||
import { title } from 'process';
|
|
||||||
import { UpdateArticle } from "../../../types/api";
|
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
console.log("articles index.ts")
|
if (req.method == "POST") { //* POST
|
||||||
if (req.method == "GET") { //* GET
|
console.log("API new article")
|
||||||
console.log("get")
|
const articleData: CreateArticle = req.body
|
||||||
const categoryName: string = req.query.categoryName?.toString() ?? "";
|
console.log(articleData)
|
||||||
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));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
} else if (req.method == "POST") { //* POST
|
if (!isValidText(articleData.title)) {
|
||||||
const data: any = req.body;
|
res.status(500).json({ target: "title", error: "Not a valid title" });
|
||||||
console.log(data)
|
|
||||||
if (!isValidText(data.title)) {
|
|
||||||
res.json({ target: "title", error: "Not a valid title" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidText(data.introduction)) {
|
if (!isValidText(articleData.introduction)) {
|
||||||
res.json({ target: "introduction", error: "Not a valid introduction" });
|
res.status(500).json({ target: "introduction", error: "Not a valid introduction" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.categoryId) {
|
if (!articleData.categoryId) {
|
||||||
res.json({ target: "category", error: "Category is required" });
|
res.status(500).json({ target: "category", error: "Category is required" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.name = formatTextToUrlName(data.title);
|
const newArticle: Prisma.ArticleUncheckedCreateInput = {
|
||||||
data.categoryId = data.categoryId.toString();
|
title: articleData.title,
|
||||||
|
name: formatTextToUrlName(articleData.title),
|
||||||
|
introduction: articleData.introduction,
|
||||||
|
categoryId: articleData.categoryId,
|
||||||
|
markdown: articleData.markdown ?? "",
|
||||||
|
contentTable: articleData.contentTable ?? {},
|
||||||
|
imageUrl: articleData.imageUrl ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
prisma.article
|
prisma.article
|
||||||
.create({ data: data, include: { category: true } })
|
.create({ data: newArticle, include: { category: true } })
|
||||||
.then(
|
.then(
|
||||||
(data) => {
|
(data: ArticleWithIncludes) => {
|
||||||
console.log("success")
|
|
||||||
res.json({ success: true, data: data });
|
res.json({ success: true, data: data });
|
||||||
},
|
},
|
||||||
(errorReason) => {
|
(errorReason) => {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import prisma from "../../../lib/prisma";
|
import prisma from "../../../lib/prisma";
|
||||||
import { Category } from "@prisma/client";
|
import { Category } from "@prisma/client";
|
||||||
import { ResponseError } from "../../../types/responseErrors";
|
import { ResponseError } from "../../../types/responseErrors";
|
||||||
@@ -7,51 +6,29 @@ import { Prisma } from "@prisma/client";
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { formatTextToUrlName } from "../../../utils";
|
import { formatTextToUrlName } from "../../../utils";
|
||||||
import { isValidText } from "../../../validators";
|
import { isValidText } from "../../../validators";
|
||||||
|
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() ?? 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") {
|
if (req.method == "PUT") {
|
||||||
const data: any = req.body;
|
const categoryId: string = req.query.categoryId?.toString() ?? "";
|
||||||
if (!isValidText(data.title)) {
|
const categoryData: UpdateCategory = req.body;
|
||||||
|
|
||||||
|
if (categoryData.title && !isValidText(categoryData.title)) {
|
||||||
res.json({ target: "title", error: "Not a valid title" });
|
res.json({ target: "title", error: "Not a valid title" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.name = formatTextToUrlName(data.title);
|
|
||||||
|
|
||||||
console.log(data);
|
|
||||||
const newSvg: Prisma.SvgUncheckedUpdateInput = {
|
const newSvg: Prisma.SvgUncheckedUpdateInput = {
|
||||||
viewbox: data.svg.viewbox,
|
viewbox: categoryData.svg?.viewbox ?? undefined,
|
||||||
path: data.svg.path,
|
path: categoryData.svg?.path ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newCategory: Prisma.CategoryUncheckedUpdateInput = {
|
const newCategory: Prisma.CategoryUncheckedUpdateInput = {
|
||||||
title: data.title,
|
title: categoryData.title ?? undefined,
|
||||||
name: data.name,
|
name: categoryData.title ? formatTextToUrlName(categoryData.title) : undefined,
|
||||||
color: data.color,
|
color: categoryData.color ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
await prisma.category
|
await prisma.category
|
||||||
@@ -62,7 +39,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
async (categoryData) => {
|
async (categoryData) => {
|
||||||
console.log("2");
|
|
||||||
await prisma.svg
|
await prisma.svg
|
||||||
.update({ data: newSvg, where: { id: categoryData.svg.id } })
|
.update({ data: newSvg, where: { id: categoryData.svg.id } })
|
||||||
.then(
|
.then(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import prisma from "../../../lib/prisma";
|
import prisma, { CategoryWithIncludes } from "../../../lib/prisma";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { Category, Svg } from "@prisma/client";
|
import { Category, Svg } from "@prisma/client";
|
||||||
import { ResponseError } from "../../../types/responseErrors";
|
import { ResponseError } from "../../../types/responseErrors";
|
||||||
@@ -7,48 +7,52 @@ import { formatTextToUrlName } from "../../../utils";
|
|||||||
import { isValidText } from "../../../validators";
|
import { isValidText } from "../../../validators";
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
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 == "GET") { //* GET
|
if (req.method == "POST") {
|
||||||
await prisma.category
|
console.log("API new category")
|
||||||
.findMany({ include: { svg: true } })
|
const categoryData: CreateCategory = req.body;
|
||||||
.then((result: Category[]) => {
|
console.log(categoryData)
|
||||||
if (result !== null) {
|
|
||||||
res.json(result);
|
if (!isValidText(categoryData.title)) {
|
||||||
} 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)) {
|
|
||||||
res.json({ target: "title", error: "Not a valid title" });
|
res.json({ target: "title", error: "Not a valid title" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.name = formatTextToUrlName(data.title);
|
categoryData.svg.viewbox = categoryData.svg.viewbox.length > 1 ? categoryData.svg.viewbox : "";
|
||||||
data.svg.viewbox = data.svg.viewbox.length > 1 ? data.svg.viewbox : null;
|
|
||||||
console.log(data);
|
const newSvg: Prisma.SvgUncheckedCreateInput = {
|
||||||
|
viewbox: categoryData.svg.viewbox,
|
||||||
|
path: categoryData.svg.path
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.svg
|
await prisma.svg
|
||||||
.create({ data: data.svg })
|
.create({ data: newSvg })
|
||||||
.then(
|
.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
|
await prisma.category
|
||||||
.create({
|
.create({
|
||||||
data: { title: data.title, name: data.name, color: data.name, svgId: svgData.id },
|
data: newCategory,
|
||||||
include: { svg: true },
|
include: { svg: true, articles: true },
|
||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
(data) => {
|
(createdCategory: CategoryWithIncludes | null) => {
|
||||||
res.json({ success: true, data: data });
|
if (createdCategory) {
|
||||||
|
res.json({ success: true, data: createdCategory });
|
||||||
|
} else {
|
||||||
|
res.json({ error: true, message: "Could not create category" });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
(errorReason) => {
|
(errorReason) => {
|
||||||
|
console.log(errorReason)
|
||||||
if (errorReason.code === "P2002") {
|
if (errorReason.code === "P2002") {
|
||||||
res.json({ target: errorReason.meta.target[0], error: "Already exists" });
|
res.json({ target: errorReason.meta.target[0], error: "Already exists" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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([]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
86
pages/articles/[categoryName]/[articleName]/index.tsx
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,16 +1,10 @@
|
|||||||
import styles from "../../../styles/modules/Category.module.scss";
|
import styles from "@/styles/modules/Category.module.scss";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArticleWithIncludes, CategoryWithIncludes, FetchManager } from "../../../manager/fetchManager";
|
import { formatTextToUrlName } from "@/utils";
|
||||||
import { formatTextToUrlName } from "../../../utils";
|
import { Article, Category } from "@prisma/client";
|
||||||
|
import prisma, { CategoryWithIncludes } from "@/lib/prisma";
|
||||||
export default async function CategoryPage({ params }: { params: { categoryName: string } }) {
|
|
||||||
const categoryName = formatTextToUrlName(params.categoryName);
|
|
||||||
const category: CategoryWithIncludes = await FetchManager.Category.get(categoryName);
|
|
||||||
|
|
||||||
const allArticles: ArticleWithIncludes[] = await FetchManager.Article.getByCategory(categoryName);
|
|
||||||
// const popularArticles: Article[] = await GetPopularArticles(categoryName);
|
|
||||||
// const recentArticles: Article[] = await GetRecentArticles(categoryName);
|
|
||||||
|
|
||||||
|
export default function CategoryPage({ category }: { category: CategoryWithIncludes | null }) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.category}>
|
<div className={styles.category}>
|
||||||
<h1>{category?.title}</h1>
|
<h1>{category?.title}</h1>
|
||||||
@@ -43,8 +37,8 @@ export default async function CategoryPage({ params }: { params: { categoryName:
|
|||||||
|
|
||||||
<div className={styles.showcase}>
|
<div className={styles.showcase}>
|
||||||
<h2>All articles</h2>
|
<h2>All articles</h2>
|
||||||
{allArticles
|
{category?.articles
|
||||||
? Array.from(allArticles).map((a, i) => {
|
? 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}`}>
|
||||||
@@ -59,3 +53,28 @@ export default async function CategoryPage({ params }: { params: { categoryName:
|
|||||||
</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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
import styles from "../../styles/modules/CategoryList.module.scss";
|
import styles from "@/styles/modules/CategoryList.module.scss";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { Category } from "@prisma/client";
|
||||||
|
import prisma, { CategoryWithIncludes } from "@/lib/prisma";
|
||||||
|
|
||||||
import { FetchManager } from "../../manager/fetchManager";
|
export default function CategoryList({ categories }: { categories: CategoryWithIncludes[] }) {
|
||||||
|
|
||||||
export default async function CategoryList() {
|
|
||||||
const categories = await FetchManager.Category.list();
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.categoryList}>
|
<div className={styles.categoryList}>
|
||||||
<h1>Overview</h1>
|
<h1>Overview</h1>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
{categories?.length > 0
|
{categories?.length > 0
|
||||||
? categories.map((cat, i) => {
|
? categories.map((cat: CategoryWithIncludes, i: number) => {
|
||||||
return (
|
return (
|
||||||
<div key={i} className={styles.linkContainer}>
|
<div key={i} className={styles.linkContainer}>
|
||||||
<Link href={`/articles/${cat.name.toLowerCase()}`} style={{ backgroundColor: cat.color }}>
|
<Link href={`/articles/${cat.name.toLowerCase()}`} style={{ backgroundColor: cat.color }}>
|
||||||
@@ -31,3 +30,21 @@ export default async function CategoryList() {
|
|||||||
</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
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- You are about to drop the column `typeId` on the `Article` table. All the data in the column will be lost.
|
|
||||||
- You are about to drop the `ArticleType` table. If the table is not empty, all the data it contains will be lost.
|
|
||||||
- You are about to drop the `ContentTableEntry` table. If the table is not empty, all the data it contains will be lost.
|
|
||||||
- Added the required column `contentTable` to the `Article` table without a default value. This is not possible if the table is not empty.
|
|
||||||
- Made the column `categoryId` on table `Article` required. This step will fail if there are existing NULL values in that column.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE "Article" DROP CONSTRAINT "Article_categoryId_fkey";
|
|
||||||
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE "Article" DROP CONSTRAINT "Article_typeId_fkey";
|
|
||||||
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE "ContentTableEntry" DROP CONSTRAINT "ContentTableEntry_articleId_fkey";
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "Article" DROP COLUMN "typeId",
|
|
||||||
ADD COLUMN "contentTable" JSONB NOT NULL,
|
|
||||||
ADD COLUMN "imageId" INTEGER,
|
|
||||||
ADD COLUMN "introduction" TEXT NOT NULL DEFAULT '',
|
|
||||||
ALTER COLUMN "categoryId" SET NOT NULL;
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "Image" ADD COLUMN "alt" TEXT NOT NULL DEFAULT '';
|
|
||||||
|
|
||||||
-- DropTable
|
|
||||||
DROP TABLE "ArticleType";
|
|
||||||
|
|
||||||
-- DropTable
|
|
||||||
DROP TABLE "ContentTableEntry";
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "Article" ADD CONSTRAINT "Article_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "Article" ADD CONSTRAINT "Article_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "Article" ALTER COLUMN "contentTable" DROP NOT NULL;
|
|
||||||
@@ -1,59 +1,40 @@
|
|||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "Article" (
|
CREATE TABLE "Article" (
|
||||||
"id" SERIAL NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"name" TEXT NOT NULL,
|
"name" TEXT NOT NULL,
|
||||||
"title" TEXT NOT NULL,
|
"title" TEXT NOT NULL,
|
||||||
|
"introduction" TEXT NOT NULL DEFAULT '',
|
||||||
|
"imageId" TEXT,
|
||||||
"markdown" TEXT NOT NULL,
|
"markdown" TEXT NOT NULL,
|
||||||
"categoryId" INTEGER,
|
"contentTable" JSONB,
|
||||||
"typeId" INTEGER,
|
"categoryId" TEXT NOT NULL,
|
||||||
"dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"dateUpdated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"dateUpdated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
CONSTRAINT "Article_pkey" PRIMARY KEY ("id")
|
CONSTRAINT "Article_pkey" PRIMARY KEY ("id")
|
||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "ContentTableEntry" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"title" TEXT NOT NULL,
|
|
||||||
"anchor" TEXT NOT NULL,
|
|
||||||
"orderIndex" INTEGER NOT NULL,
|
|
||||||
"articleId" INTEGER NOT NULL,
|
|
||||||
"dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"dateUpdated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
CONSTRAINT "ContentTableEntry_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "Category" (
|
CREATE TABLE "Category" (
|
||||||
"id" SERIAL NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"name" TEXT NOT NULL,
|
"name" TEXT NOT NULL,
|
||||||
"title" TEXT NOT NULL,
|
"title" TEXT NOT NULL,
|
||||||
"color" TEXT NOT NULL,
|
"color" TEXT NOT NULL,
|
||||||
"svgId" INTEGER NOT NULL,
|
"svgId" TEXT NOT NULL,
|
||||||
"dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"dateUpdated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"dateUpdated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
|
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
|
||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "ArticleType" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"title" TEXT NOT NULL,
|
|
||||||
"dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"dateUpdated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
CONSTRAINT "ArticleType_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "Image" (
|
CREATE TABLE "Image" (
|
||||||
"id" SERIAL NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"name" TEXT NOT NULL,
|
"name" TEXT NOT NULL,
|
||||||
|
"alt" TEXT NOT NULL DEFAULT '',
|
||||||
"url" TEXT NOT NULL DEFAULT '',
|
"url" TEXT NOT NULL DEFAULT '',
|
||||||
|
"width" INTEGER NOT NULL,
|
||||||
|
"height" INTEGER NOT NULL,
|
||||||
"dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
CONSTRAINT "Image_pkey" PRIMARY KEY ("id")
|
CONSTRAINT "Image_pkey" PRIMARY KEY ("id")
|
||||||
@@ -61,8 +42,7 @@ CREATE TABLE "Image" (
|
|||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "Svg" (
|
CREATE TABLE "Svg" (
|
||||||
"id" SERIAL NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"path" TEXT NOT NULL DEFAULT '',
|
"path" TEXT NOT NULL DEFAULT '',
|
||||||
"viewbox" TEXT NOT NULL DEFAULT '0 0 512 512',
|
"viewbox" TEXT NOT NULL DEFAULT '0 0 512 512',
|
||||||
|
|
||||||
@@ -81,29 +61,14 @@ CREATE UNIQUE INDEX "Category_name_key" ON "Category"("name");
|
|||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "Category_title_key" ON "Category"("title");
|
CREATE UNIQUE INDEX "Category_title_key" ON "Category"("title");
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "Category_color_key" ON "Category"("color");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "ArticleType_name_key" ON "ArticleType"("name");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "ArticleType_title_key" ON "ArticleType"("title");
|
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "Image_name_key" ON "Image"("name");
|
CREATE UNIQUE INDEX "Image_name_key" ON "Image"("name");
|
||||||
|
|
||||||
-- CreateIndex
|
-- AddForeignKey
|
||||||
CREATE UNIQUE INDEX "Svg_name_key" ON "Svg"("name");
|
ALTER TABLE "Article" ADD CONSTRAINT "Article_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "Article" ADD CONSTRAINT "Article_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
ALTER TABLE "Article" ADD CONSTRAINT "Article_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "Article" ADD CONSTRAINT "Article_typeId_fkey" FOREIGN KEY ("typeId") REFERENCES "ArticleType"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "ContentTableEntry" ADD CONSTRAINT "ContentTableEntry_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "Category" ADD CONSTRAINT "Category_svgId_fkey" FOREIGN KEY ("svgId") REFERENCES "Svg"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
ALTER TABLE "Category" ADD CONSTRAINT "Category_svgId_fkey" FOREIGN KEY ("svgId") REFERENCES "Svg"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
17
prisma/migrations/20230207114149_no_image/migration.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `imageId` on the `Article` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the `Image` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- Added the required column `imageUrl` to the `Article` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Article" DROP CONSTRAINT "Article_imageId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Article" DROP COLUMN "imageId",
|
||||||
|
ADD COLUMN "imageUrl" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "Image";
|
||||||
@@ -12,8 +12,7 @@ model Article {
|
|||||||
name String @unique
|
name String @unique
|
||||||
title String @unique
|
title String @unique
|
||||||
introduction String @default("")
|
introduction String @default("")
|
||||||
imageId String?
|
imageUrl String
|
||||||
image Image? @relation(fields: [imageId], references: [id])
|
|
||||||
markdown String
|
markdown String
|
||||||
contentTable Json?
|
contentTable Json?
|
||||||
categoryId String
|
categoryId String
|
||||||
@@ -29,22 +28,11 @@ model Category {
|
|||||||
color String
|
color String
|
||||||
svgId String
|
svgId String
|
||||||
svg Svg @relation(fields: [svgId], references: [id])
|
svg Svg @relation(fields: [svgId], references: [id])
|
||||||
Article Article[]
|
articles Article[]
|
||||||
dateCreated DateTime @default(now())
|
dateCreated DateTime @default(now())
|
||||||
dateUpdated DateTime @default(now())
|
dateUpdated DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
model Image {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
name String @unique
|
|
||||||
alt String @default("")
|
|
||||||
url String @default("")
|
|
||||||
width Int
|
|
||||||
height Int
|
|
||||||
dateCreated DateTime @default(now())
|
|
||||||
Article Article[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model Svg {
|
model Svg {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
path String @default("")
|
path String @default("")
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
zrpmayg1wugrgxpm
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
[{
|
|
||||||
"SourceFile": "/photoprism/storage/users/urpmb0z2xmpahuqx/upload/sesssmy6hfc1umzov2/Berlin_Brandenburger_Tor_im_Sonnenuntergang_Leitmotiv_German_Summer_Cities.jpg",
|
|
||||||
"ExifToolVersion": 12.40,
|
|
||||||
"FileName": "Berlin_Brandenburger_Tor_im_Sonnenuntergang_Leitmotiv_German_Summer_Cities.jpg",
|
|
||||||
"Directory": "/photoprism/storage/users/urpmb0z2xmpahuqx/upload/sesssmy6hfc1umzov2",
|
|
||||||
"FileSize": 246578,
|
|
||||||
"FileModifyDate": "2023:02:05 17:25:02+00:00",
|
|
||||||
"FileAccessDate": "2023:02:05 17:25:02+00:00",
|
|
||||||
"FileInodeChangeDate": "2023:02:05 17:25:02+00:00",
|
|
||||||
"FilePermissions": 100664,
|
|
||||||
"FileType": "JPEG",
|
|
||||||
"FileTypeExtension": "JPG",
|
|
||||||
"MIMEType": "image/jpeg",
|
|
||||||
"JFIFVersion": "1 1",
|
|
||||||
"ResolutionUnit": 0,
|
|
||||||
"XResolution": 1,
|
|
||||||
"YResolution": 1,
|
|
||||||
"ImageWidth": 1600,
|
|
||||||
"ImageHeight": 900,
|
|
||||||
"EncodingProcess": 2,
|
|
||||||
"BitsPerSample": 8,
|
|
||||||
"ColorComponents": 3,
|
|
||||||
"YCbCrSubSampling": "1 1",
|
|
||||||
"ImageSize": "1600 900",
|
|
||||||
"Megapixels": 1.44
|
|
||||||
}]
|
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 102 B |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 71 KiB |
@@ -1,6 +0,0 @@
|
|||||||
Version: 221118-e58fee0fb-Linux-AMD64
|
|
||||||
Key: 0427e51091ac43eaaa55978febab4e2112bad60c
|
|
||||||
Secret: be0711bf10f092235e039884f0039400
|
|
||||||
Session: 54a3619247ffec19480e915f526d69bfd3b82297b3d722fafafe9b60b897e45fe90bc57a500989ca4aa5baef53f1063b5fa5dc45ebeb864f575005341e9186832bd2915f576fb192223ca304c439313aba793c82faae63402518682510396386b8
|
|
||||||
Status: ce
|
|
||||||
Serial: zrpmayg1wugrgxpm
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
UI:
|
|
||||||
Scrollbar: true
|
|
||||||
Zoom: false
|
|
||||||
Theme: default
|
|
||||||
Language: en
|
|
||||||
TimeZone: ""
|
|
||||||
Search:
|
|
||||||
BatchSize: 0
|
|
||||||
Maps:
|
|
||||||
Animate: 0
|
|
||||||
Style: ""
|
|
||||||
Features:
|
|
||||||
Account: true
|
|
||||||
Advanced: false
|
|
||||||
Albums: true
|
|
||||||
Archive: true
|
|
||||||
Delete: false
|
|
||||||
Download: true
|
|
||||||
Edit: true
|
|
||||||
Estimates: false
|
|
||||||
Favorites: true
|
|
||||||
Files: true
|
|
||||||
Folders: true
|
|
||||||
Import: true
|
|
||||||
Labels: false
|
|
||||||
Library: false
|
|
||||||
Logs: true
|
|
||||||
Moments: false
|
|
||||||
People: false
|
|
||||||
Places: false
|
|
||||||
Private: true
|
|
||||||
Ratings: true
|
|
||||||
Reactions: true
|
|
||||||
Review: false
|
|
||||||
Search: true
|
|
||||||
Services: true
|
|
||||||
Settings: true
|
|
||||||
Share: true
|
|
||||||
Upload: true
|
|
||||||
Videos: true
|
|
||||||
Import:
|
|
||||||
Path: /
|
|
||||||
Move: false
|
|
||||||
Index:
|
|
||||||
Path: /
|
|
||||||
Convert: true
|
|
||||||
Rescan: false
|
|
||||||
SkipArchived: false
|
|
||||||
Stack:
|
|
||||||
UUID: true
|
|
||||||
Meta: true
|
|
||||||
Name: false
|
|
||||||
Share:
|
|
||||||
Title: ""
|
|
||||||
Download:
|
|
||||||
Name: file
|
|
||||||
Disabled: false
|
|
||||||
Originals: true
|
|
||||||
MediaRaw: false
|
|
||||||
MediaSidecar: false
|
|
||||||
Templates:
|
|
||||||
Default: index.gohtml
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
zrpmayg1wugrgxpm
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
TakenAt: 2023-02-05T17:25:04Z
|
|
||||||
UID: prpmb1v3h8wlphpy
|
|
||||||
Type: image
|
|
||||||
Title: Berlin Brandenburger Tor Im Sonnenuntergang Leitmotiv German Summer Cities
|
|
||||||
OriginalName: Berlin_Brandenburger_Tor_im_Sonnenuntergang_Leitmotiv_German_Summer_Cities
|
|
||||||
TimeZone: UTC
|
|
||||||
Year: -1
|
|
||||||
Month: -1
|
|
||||||
Day: -1
|
|
||||||
Quality: 1
|
|
||||||
Details:
|
|
||||||
Keywords: berlin, brandenburger, cities, german, gold, historic, leitmotiv, sonnenuntergang,
|
|
||||||
summer, tor
|
|
||||||
CreatedBy: urpmb0z2xmpahuqx
|
|
||||||
CreatedAt: 2023-02-05T17:25:07.100920585Z
|
|
||||||
UpdatedAt: 2023-02-05T17:25:07.254378706Z
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
@import "../variables.scss";
|
|
||||||
|
|
||||||
.adminControl {
|
|
||||||
border-bottom: 2px solid var(--color-danger);
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
column-gap: 50px;
|
|
||||||
margin-top: -10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
.adminNav {
|
.adminNav {
|
||||||
border-top: 2px solid var(--color-danger);
|
border-top: 2px solid var(--color-danger);
|
||||||
|
border-bottom: 2px solid var(--color-danger);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -4,23 +4,21 @@
|
|||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": false,
|
"strict": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"incremental": true,
|
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noImplicitThis": true,
|
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"plugins": [
|
"incremental": true,
|
||||||
{
|
"baseUrl": ".",
|
||||||
"name": "next"
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ export interface CreateArticle {
|
|||||||
title: string;
|
title: string;
|
||||||
markdown: string;
|
markdown: string;
|
||||||
introduction: string;
|
introduction: string;
|
||||||
categoryId: number;
|
categoryId: string;
|
||||||
contentTable: Prisma.JsonArray
|
contentTable: Prisma.JsonArray
|
||||||
imageId?: number;
|
imageUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateArticle {
|
export interface UpdateArticle {
|
||||||
@@ -15,9 +15,9 @@ export interface UpdateArticle {
|
|||||||
title?: string;
|
title?: string;
|
||||||
markdown?: string;
|
markdown?: string;
|
||||||
introduction?: string;
|
introduction?: string;
|
||||||
categoryId?: number;
|
categoryId?: string;
|
||||||
contentTable?: Prisma.JsonArray
|
contentTable?: Prisma.JsonArray
|
||||||
imageId?: number;
|
imageUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateCategory {
|
export interface CreateCategory {
|
||||||
|
|||||||