refactor
8
.gitignore
vendored
@@ -4,7 +4,7 @@
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
/copy
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
/docker_data/postgres/
|
||||
|
||||
#! .env
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
@@ -36,5 +34,3 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
.vscode
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"printWidth": 120
|
||||
"printWidth": 180
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ First, run the development server:
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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 Link from "next/link";
|
||||
import styles from "../styles/modules/AdminNav.module.scss";
|
||||
import styles from "@/styles/modules/AdminNav.module.scss";
|
||||
|
||||
function AdminNav() {
|
||||
return (
|
||||
<div className={styles.adminNav}>
|
||||
<Link href={"/admin"}>Admin</Link>
|
||||
<Link href={"/admin/articles/editor/0"}>New article</Link>
|
||||
<Link href={"/admin/categories/editor/0"}>New category</Link>
|
||||
<Link href={"/admin/editor/article/0"}>New article</Link>
|
||||
<Link href={"/admin/editor/category/0"}>New category</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
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 }) {
|
||||
return (
|
||||
<div className={styles.articleContentTable}>
|
||||
<div className={styles.stickyContainer}>
|
||||
<div className={styles.list}>
|
||||
<h2>Contents</h2>
|
||||
{contentTableData?.map((e, i) => {
|
||||
return (
|
||||
<a key={i} href={"#" + e.anchor}>
|
||||
{e.title}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{contentTableData?.length < 15 ? <div className={styles.adContainer}>Future advertisement</div> : ""}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
export default function ContentTable({
|
||||
contentTableData,
|
||||
}: {
|
||||
contentTableData: any;
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.articleContentTable}>
|
||||
<div className={styles.stickyContainer}>
|
||||
<div className={styles.list}>
|
||||
<h2>Contents</h2>
|
||||
{contentTableData?.map((e: IContentTableEntry, i: number) => {
|
||||
return (
|
||||
<a key={i} href={"#" + e.anchor}>
|
||||
{e.title}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{contentTableData?.length < 15 ? (
|
||||
<div className={styles.adContainer}>Future advertisement</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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";
|
||||
export default function Footer() {
|
||||
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 ReactMarkdown from "react-markdown";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import oneDark from "react-syntax-highlighter/dist/esm/styles/prism/one-dark";
|
||||
import oneLight from "react-syntax-highlighter/dist/esm/styles/prism/one-light";
|
||||
import oneDark from "react-syntax-highlighter/dist/cjs/styles/prism/one-dark";
|
||||
import oneLight from "react-syntax-highlighter/dist/cjs/styles/prism/one-light";
|
||||
import styles from "../styles/modules/markdown.module.scss";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkGemoji from "remark-gemoji";
|
||||
@@ -13,11 +13,11 @@ import React from "react";
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function HeadingRenderer({ children, level }) {
|
||||
function HeadingRenderer({ children, level }: { children: any; level: any }) {
|
||||
children = React.Children.toArray(children);
|
||||
const text = children.reduce(flatten, "");
|
||||
return React.createElement("h" + level, { id: formatTextToUrlName(text) }, children);
|
||||
@@ -55,7 +55,13 @@ export default function Markdown({ value }: { value: any }) {
|
||||
</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$/, "")}
|
||||
</SyntaxHighlighter>
|
||||
</>
|
||||
|
||||
@@ -1,135 +1,149 @@
|
||||
"use client";
|
||||
import styles from "../styles/modules/Nav.module.scss";
|
||||
import styles from "@/styles/modules/Nav.module.scss";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Category } from "@prisma/client";
|
||||
|
||||
function switchTheme(theme) {
|
||||
const bodyElement = document.getElementsByTagName("body")[0];
|
||||
function switchTheme(theme: string) {
|
||||
const bodyElement = document.getElementsByTagName("body")[0];
|
||||
|
||||
if (theme == "dark") {
|
||||
bodyElement.classList.remove("theme-light");
|
||||
} else {
|
||||
bodyElement.classList.add("theme-light");
|
||||
}
|
||||
if (theme == "dark") {
|
||||
bodyElement.classList.remove("theme-light");
|
||||
} else {
|
||||
bodyElement.classList.add("theme-light");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const svgElement = document.getElementById("themeSwitchSvg");
|
||||
const svgElement = document.getElementById("themeSwitchSvg");
|
||||
|
||||
if (localStorage.getItem("theme") == "light") {
|
||||
svgElement.style.animationDirection = "normal";
|
||||
svgElement.style.animationName = styles.spinThemeSwitch;
|
||||
} else {
|
||||
svgElement.style.animationDirection = "reverse";
|
||||
svgElement.style.animationName = styles.spinThemeSwitch;
|
||||
}
|
||||
if (svgElement) {
|
||||
if (localStorage.getItem("theme") == "light") {
|
||||
svgElement.style.animationDirection = "normal";
|
||||
svgElement.style.animationName = styles.spinThemeSwitch;
|
||||
} else {
|
||||
svgElement.style.animationDirection = "reverse";
|
||||
svgElement.style.animationName = styles.spinThemeSwitch;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (localStorage.getItem("theme") == "light") {
|
||||
localStorage.setItem("theme", "dark");
|
||||
switchTheme("dark");
|
||||
} else {
|
||||
localStorage.setItem("theme", "light");
|
||||
switchTheme("light");
|
||||
}
|
||||
svgElement.style.animationName = "";
|
||||
}, 150);
|
||||
setTimeout(() => {
|
||||
if (localStorage.getItem("theme") == "light") {
|
||||
localStorage.setItem("theme", "dark");
|
||||
switchTheme("dark");
|
||||
} else {
|
||||
localStorage.setItem("theme", "light");
|
||||
switchTheme("light");
|
||||
}
|
||||
svgElement.style.animationName = "";
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
|
||||
export default function Nav({ categories }: { categories: Category[] }) {
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [searchResults, setSearchResults] = useState<
|
||||
{ name: string; title: string }[]
|
||||
>([]);
|
||||
|
||||
async function handleSearchInput(event) {
|
||||
const query = event.target.value;
|
||||
let result = await fetch(`/api/search?q=${query}`);
|
||||
let json = await result.json();
|
||||
async function handleSearchInput(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const query = event.target.value;
|
||||
let result = await fetch(`/api/search?q=${query}`);
|
||||
let json = await result.json();
|
||||
|
||||
if (json.length == 0 && query.length > 0) {
|
||||
setSearchResults([{ name: "", title: "No article found..." }]);
|
||||
} else {
|
||||
setSearchResults(json);
|
||||
}
|
||||
}
|
||||
if (json.length == 0 && query.length > 0) {
|
||||
setSearchResults([{ name: "", title: "No article found..." }]);
|
||||
} else {
|
||||
setSearchResults(json);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem("theme") == "dark") {
|
||||
switchTheme("dark");
|
||||
} else {
|
||||
switchTheme("light");
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem("theme") == "dark") {
|
||||
switchTheme("dark");
|
||||
} else {
|
||||
switchTheme("light");
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(searchResults);
|
||||
}, [searchResults]);
|
||||
useEffect(() => {
|
||||
console.log(searchResults);
|
||||
}, [searchResults]);
|
||||
|
||||
return (
|
||||
<nav className={styles.nav}>
|
||||
<div className={styles.containerLeft}>
|
||||
<Image
|
||||
src={"/images/logo.svg"}
|
||||
height={40}
|
||||
width={160}
|
||||
alt="Nav bar logo"
|
||||
onClick={() => {
|
||||
window.open("/", "_self");
|
||||
}}
|
||||
className={styles.logo}
|
||||
/>
|
||||
<div className={styles.links}>
|
||||
<div className={styles.dropDown}>
|
||||
<Link href="/articles">Categories</Link>
|
||||
<div className={styles.dropDownContainer}>
|
||||
<div className={styles.content}>
|
||||
<Link href={"/articles"}>All</Link>
|
||||
return (
|
||||
<nav className={styles.nav}>
|
||||
<div className={styles.containerLeft}>
|
||||
<Image
|
||||
src={"/images/logo.svg"}
|
||||
height={40}
|
||||
width={160}
|
||||
alt="Nav bar logo"
|
||||
onClick={() => {
|
||||
window.open("/", "_self");
|
||||
}}
|
||||
className={styles.logo}
|
||||
/>
|
||||
<div className={styles.links}>
|
||||
<div className={styles.dropDown}>
|
||||
<Link href="/articles">Categories</Link>
|
||||
<div className={styles.dropDownContainer}>
|
||||
<div className={styles.content}>
|
||||
<Link href={"/articles"}>All</Link>
|
||||
|
||||
{categories?.map((cat, i) => {
|
||||
{
|
||||
return (
|
||||
<Link key={i} href={`/articles/${cat.name.toLowerCase()}`}>
|
||||
{cat.title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.containerCenter}>
|
||||
<div className={styles.searchBar}>
|
||||
<div className={styles.icon}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352c79.5 0 144-64.5 144-144s-64.5-144-144-144S64 128.5 64 208s64.5 144 144 144z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input onInput={handleSearchInput} type="text" name="" id="" />
|
||||
</div>
|
||||
<div className={styles.searchResults}>
|
||||
<div className={styles.content}>
|
||||
{searchResults.map((s) => {
|
||||
{
|
||||
return <Link href={`/articles/${s.name.toLowerCase()}`}>{s.title}</Link>;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.containerRight}>
|
||||
<div
|
||||
className={styles.themeSwitch}
|
||||
onClick={() => {
|
||||
toggleTheme();
|
||||
}}
|
||||
>
|
||||
<svg id="themeSwitchSvg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M448 256c0-106-86-192-192-192V448c106 0 192-86 192-192zm64 0c0 141.4-114.6 256-256 256S0 397.4 0 256S114.6 0 256 0S512 114.6 512 256z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
{categories?.map((cat, i) => {
|
||||
{
|
||||
return (
|
||||
<Link
|
||||
key={i}
|
||||
href={`/articles/${cat.name.toLowerCase()}`}
|
||||
>
|
||||
{cat.title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.containerCenter}>
|
||||
<div className={styles.searchBar}>
|
||||
<div className={styles.icon}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352c79.5 0 144-64.5 144-144s-64.5-144-144-144S64 128.5 64 208s64.5 144 144 144z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input onInput={handleSearchInput} type="text" name="" id="" />
|
||||
</div>
|
||||
<div className={styles.searchResults}>
|
||||
<div className={styles.content}>
|
||||
{searchResults.map((s) => {
|
||||
{
|
||||
return (
|
||||
<Link href={`/articles/${s.name.toLowerCase()}`}>
|
||||
{s.title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.containerRight}>
|
||||
<div
|
||||
className={styles.themeSwitch}
|
||||
onClick={() => {
|
||||
toggleTheme();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
id="themeSwitchSvg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path d="M448 256c0-106-86-192-192-192V448c106 0 192-86 192-192zm64 0c0 141.4-114.6 256-256 256S0 397.4 0 256S114.6 0 256 0S512 114.6 512 256z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import React from "react";
|
||||
import styles from "../styles/modules/Sidebar.module.scss";
|
||||
import styles from "@/styles/modules/Sidebar.module.scss";
|
||||
export default function Sidebar() {
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<div className={styles.stickyContainer}>
|
||||
<div className={styles.sidebarContainer}>
|
||||
<h3>Popular</h3>
|
||||
<a href="#"> Set up Docker</a>
|
||||
<a href="#"> Set up Docker</a>
|
||||
<a href="#"> Set up Docker</a>
|
||||
<a href="#"> Set up Docker</a>
|
||||
<a href="#"> Set up Docker</a>
|
||||
</div>
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<div className={styles.stickyContainer}>
|
||||
<div className={styles.sidebarContainer}>
|
||||
<h3>Popular</h3>
|
||||
<a href="#"> Set up Docker</a>
|
||||
<a href="#"> Set up Docker</a>
|
||||
<a href="#"> Set up Docker</a>
|
||||
<a href="#"> Set up Docker</a>
|
||||
<a href="#"> Set up Docker</a>
|
||||
</div>
|
||||
|
||||
<div className={styles.sidebarContainer}>
|
||||
<h3>Related</h3>
|
||||
<a href="#"> Set up Docker</a>
|
||||
<a href="#"> Set up Docker</a>
|
||||
<a href="#"> Set up Docker</a>
|
||||
<a href="#"> Set up Docker</a>
|
||||
<a href="#"> Set up Docker</a>
|
||||
</div>
|
||||
<div className={styles.adContainer}>Future advertisement</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className={styles.sidebarContainer}>
|
||||
<h3>Related</h3>
|
||||
<a href="#"> Set up Docker</a>
|
||||
<a href="#"> Set up Docker</a>
|
||||
<a href="#"> Set up Docker</a>
|
||||
<a href="#"> Set up Docker</a>
|
||||
<a href="#"> Set up Docker</a>
|
||||
</div>
|
||||
<div className={styles.adContainer}>Future advertisement</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
prisma = new PrismaClient();
|
||||
} else {
|
||||
if (!global.prisma) {
|
||||
global.prisma = new PrismaClient();
|
||||
if (typeof window === "undefined") {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
prisma = new PrismaClient();
|
||||
} else {
|
||||
// @ts-ignore
|
||||
if (!global.prisma) {
|
||||
// @ts-ignore
|
||||
global.prisma = new PrismaClient();
|
||||
}
|
||||
// @ts-ignore
|
||||
prisma = global.prisma;
|
||||
}
|
||||
prisma = global.prisma;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
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} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
appDir: true,
|
||||
},
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
7837
package-lock.json
generated
49
package.json
@@ -1,55 +1,38 @@
|
||||
{
|
||||
"name": "explainegy",
|
||||
"version": "0.2.0",
|
||||
"name": "explainegy_pages",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prisma": "prisma generate && prisma db push && prisma studio",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"vercel-build": "prisma generate && prisma db push && next build",
|
||||
"prisma:generate": "prisma generate"
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/font": "13.0.7",
|
||||
"@next/font": "13.1.6",
|
||||
"@prisma/client": "^4.9.0",
|
||||
"@types/express": "^4.17.15",
|
||||
"@types/formidable": "^2.0.5",
|
||||
"@types/marked": "^4.0.8",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"encoding": "^0.1.13",
|
||||
"eslint": "8.30.0",
|
||||
"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",
|
||||
"@types/node": "18.11.19",
|
||||
"@types/react": "18.0.27",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-config-next": "13.1.6",
|
||||
"next": "13.1.6",
|
||||
"next-superjson-plugin": "^0.5.4",
|
||||
"react": "18.2.0",
|
||||
"react-code-blocks": "^0.0.9-0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-grid-gallery": "^1.0.0",
|
||||
"react-markdown": "^8.0.4",
|
||||
"react-markdown": "^8.0.5",
|
||||
"react-select": "^5.7.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-gfm": "^3.0.1",
|
||||
"remark-stringify": "^10.0.2",
|
||||
"sass": "^1.57.0",
|
||||
"typescript": "4.9.4",
|
||||
"sass": "^1.58.0",
|
||||
"typescript": "4.9.5",
|
||||
"url-join": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fec/remark-a11y-emoji": "^3.1.0",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^18.11.17",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/react-select": "^5.0.1",
|
||||
"@types/react-syntax-highlighter": "^15.5.6",
|
||||
"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 { useState, useRef, useEffect } from "react";
|
||||
import styles from "../../../../../styles/modules/ArticleEditor.module.scss";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import "../../../../../styles/inputs.scss";
|
||||
import "../../../../../styles/buttons.scss";
|
||||
import styles from "@/styles/modules/ArticleEditor.module.scss";
|
||||
import { Prisma, Category } from "@prisma/client";
|
||||
import Select from "react-select";
|
||||
import { useRouter } from "next/navigation";
|
||||
import urlJoin from "url-join";
|
||||
import { IContentTableEntry } from "../../../../../types/contentTable";
|
||||
import { CreateArticle, UpdateArticle } from "../../../../../types/api";
|
||||
import { formatTextToUrlName } from "../../../../../utils";
|
||||
import { isValidText } from "../../../../../validators";
|
||||
import { apiUrl } from "../../../../../global";
|
||||
import Markdown from "../../../../../components/Markdown";
|
||||
import { IContentTableEntry } from "@/types/contentTable";
|
||||
import { CreateArticle, UpdateArticle } from "@/types/api";
|
||||
import { formatTextToUrlName } from "@/utils";
|
||||
import { isValidText } from "@/validators";
|
||||
import { apiUrl } from "@/global";
|
||||
import Markdown from "@/components/Markdown";
|
||||
import prisma, { ArticleWithIncludes, CategoryWithIncludes } from "@/lib/prisma";
|
||||
import { CLIENT_RENEG_LIMIT } from "tls";
|
||||
|
||||
type ArticleWithCategory = Prisma.ArticleGetPayload<{ include: { category: true } }>;
|
||||
|
||||
export default function AdminArticlesEditorPage({ params }: { params: { articleId: string } }) {
|
||||
export default function AdminArticlesEditorPage({ article, categories }: { article: ArticleWithIncludes | null; categories: CategoryWithIncludes[] }) {
|
||||
const router = useRouter();
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [selectCategoriesOptions, setSelectCategoriesOptions] = useState<any>([]);
|
||||
const [introduction, setIntroduction] = useState<string>("");
|
||||
const [markdown, setMarkdown] = useState<string>("");
|
||||
const [contentTable, setContentTable] = useState<any>([]);
|
||||
|
||||
const [title, setTitle] = useState<string>(article?.title ?? "");
|
||||
const [selectCategoriesOptions, setSelectCategoriesOptions] = useState<{ value: string; label: string }[]>(
|
||||
categories?.map((c: CategoryWithIncludes) => ({ value: c.id, label: c.title }))
|
||||
);
|
||||
|
||||
const [introduction, setIntroduction] = useState<string>(article?.introduction ?? "");
|
||||
const [markdown, setMarkdown] = useState<string>(article?.markdown ?? "");
|
||||
const [contentTable, setContentTable] = useState<any>(article?.contentTable ?? []);
|
||||
|
||||
const titleRef = useRef<HTMLInputElement>(null);
|
||||
const categorySelectRef = useRef(null);
|
||||
const categorySelectRef = useRef<any>(null);
|
||||
const introductionRef = useRef<HTMLInputElement>(null);
|
||||
const markdownTextAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const errorTextRef = useRef(null);
|
||||
const errorTextRef = useRef<HTMLParagraphElement>(null);
|
||||
|
||||
function changeContentTableEntryAnchor(index: number, newAnchor: string) {
|
||||
setContentTable((prevArray) => {
|
||||
setContentTable((prevArray: any) => {
|
||||
let newArray = [...prevArray];
|
||||
newArray[index].anchor = newAnchor;
|
||||
return newArray;
|
||||
@@ -42,7 +42,7 @@ export default function AdminArticlesEditorPage({ params }: { params: { articleI
|
||||
}
|
||||
|
||||
function changeContentTableEntryTitle(index: number, newTitle: string) {
|
||||
setContentTable((prevArray) => {
|
||||
setContentTable((prevArray: any) => {
|
||||
let newArray = [...prevArray];
|
||||
newArray[index].anchor = newTitle;
|
||||
return newArray;
|
||||
@@ -55,18 +55,15 @@ export default function AdminArticlesEditorPage({ params }: { params: { articleI
|
||||
setContentTable(newArray);
|
||||
}
|
||||
|
||||
function handleFormChange() {
|
||||
setMarkdown(markdownTextAreaRef.current.value);
|
||||
setTitle(titleRef.current.value);
|
||||
setIntroduction(introductionRef.current.value);
|
||||
}
|
||||
|
||||
// Create or update article
|
||||
async function handleResponse(res: Response) {
|
||||
const json = await res.json();
|
||||
errorTextRef.current.innerText = json.error ?? "";
|
||||
if (errorTextRef?.current) {
|
||||
errorTextRef.current.innerText = json.error ?? "";
|
||||
}
|
||||
|
||||
if (json.success) {
|
||||
const newArticle: ArticleWithCategory = json.data;
|
||||
const newArticle: ArticleWithIncludes = json.data;
|
||||
router.push(urlJoin(`/articles/`, newArticle.category.name, newArticle.name));
|
||||
}
|
||||
}
|
||||
@@ -74,15 +71,15 @@ export default function AdminArticlesEditorPage({ params }: { params: { articleI
|
||||
async function updateArticle() {
|
||||
console.log("Update article");
|
||||
const payload: UpdateArticle = {
|
||||
title: titleRef.current.value,
|
||||
introduction: introductionRef.current.value,
|
||||
title: titleRef?.current?.value,
|
||||
introduction: introductionRef?.current?.value,
|
||||
markdown: markdown,
|
||||
categoryId: Number(categorySelectRef?.current?.getValue()[0]?.value),
|
||||
categoryId: categorySelectRef?.current?.getValue()[0]?.value,
|
||||
contentTable: contentTable,
|
||||
};
|
||||
console.log(payload);
|
||||
|
||||
await fetch(`/api/articles/${params.articleId.toString()}`, {
|
||||
await fetch(`/api/articles/${article?.id.toString()}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
@@ -98,10 +95,10 @@ export default function AdminArticlesEditorPage({ params }: { params: { articleI
|
||||
async function createArticle() {
|
||||
console.log("Create article");
|
||||
const payload: CreateArticle = {
|
||||
title: titleRef.current.value,
|
||||
introduction: introductionRef.current.value,
|
||||
title: titleRef?.current?.value ?? "",
|
||||
introduction: introductionRef?.current?.value ?? "",
|
||||
markdown: markdown,
|
||||
categoryId: Number(categorySelectRef?.current?.getValue()[0]?.value),
|
||||
categoryId: categorySelectRef?.current?.getValue()[0]?.value,
|
||||
contentTable: contentTable,
|
||||
};
|
||||
console.log(payload);
|
||||
@@ -119,73 +116,22 @@ export default function AdminArticlesEditorPage({ params }: { params: { articleI
|
||||
.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 (
|
||||
<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}>
|
||||
<p className="text-error" ref={errorTextRef}></p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (params.articleId != "0") {
|
||||
if (article) {
|
||||
updateArticle();
|
||||
} else {
|
||||
createArticle();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{params.articleId == "0" ? "Create article" : "Update article"}
|
||||
{article ? "Update article" : "Create article"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -196,21 +142,17 @@ export default function AdminArticlesEditorPage({ params }: { params: { articleI
|
||||
|
||||
<div className={styles.titleInputs}>
|
||||
<input
|
||||
onChange={handleFormChange}
|
||||
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}
|
||||
onChange={handleFormChange}
|
||||
className={""}
|
||||
type="text"
|
||||
name="name"
|
||||
value={title ? formatTextToUrlName(title) : ""}
|
||||
/>
|
||||
<input readOnly={true} className={""} type="text" name="name" value={title ? formatTextToUrlName(title) : ""} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.category}>
|
||||
@@ -219,25 +161,34 @@ export default function AdminArticlesEditorPage({ params }: { params: { articleI
|
||||
ref={categorySelectRef}
|
||||
className="react-select-container"
|
||||
classNamePrefix="react-select"
|
||||
onChange={handleFormChange}
|
||||
options={selectCategoriesOptions}
|
||||
defaultValue={article ? { value: article.category.id, label: article.category.title } : {}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.introduction}>
|
||||
<label htmlFor="title">Introduction</label>
|
||||
<input
|
||||
onChange={handleFormChange}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIntroduction(e.target.value);
|
||||
}}
|
||||
className={!isValidText(introduction) && introduction ? "error" : ""}
|
||||
type="text"
|
||||
name="introduction"
|
||||
placeholder="Introduction"
|
||||
ref={introductionRef}
|
||||
defaultValue={introduction}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.markdown}>
|
||||
<label htmlFor="">Markdown Editor</label>
|
||||
<div className={styles.markdownEditor}>
|
||||
<textarea ref={markdownTextAreaRef} onChange={handleFormChange}></textarea>
|
||||
<textarea
|
||||
ref={markdownTextAreaRef}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setMarkdown(e.target.value);
|
||||
}}
|
||||
defaultValue={markdown}
|
||||
></textarea>
|
||||
<Markdown value={markdown} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -292,3 +243,43 @@ export default function AdminArticlesEditorPage({ params }: { params: { articleI
|
||||
</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 } }>
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const articleId: string = formatTextToUrlName(req.query.articleId.toString())
|
||||
|
||||
console.log(`API articleId: ${articleId}`)
|
||||
if (req.method == "GET") { //* GET
|
||||
console.log("get")
|
||||
await prisma.article
|
||||
.findUnique({ where: { id: articleId }, include: { category: true, image: true } })
|
||||
.then((result: ArticleWithIncludes) => {
|
||||
if (result !== null) {
|
||||
console.log("result", result)
|
||||
res.json(result);
|
||||
} else {
|
||||
console.log("no article found")
|
||||
const error: ResponseError = {
|
||||
code: "404",
|
||||
message: "No article with this name found!",
|
||||
};
|
||||
res.status(404).json(error);
|
||||
}
|
||||
}, (err) => {
|
||||
console.log("reason", err)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("catch", err)
|
||||
const error: ResponseError = {
|
||||
code: "500",
|
||||
message: err,
|
||||
};
|
||||
res.status(500).json(error);
|
||||
});
|
||||
} else if (req.method == "PUT") {//* PUT
|
||||
if (req.method == "PUT") {//* PUT
|
||||
console.log("PUT")
|
||||
const data: UpdateArticle = req.body;
|
||||
const articleId: string = formatTextToUrlName(req.query?.articleId?.toString() ?? "")
|
||||
|
||||
if (!isValidText(data.title)) {
|
||||
console.log(`API articleId: ${articleId}`)
|
||||
const articleData: UpdateArticle = req.body;
|
||||
|
||||
if (articleData.title && !isValidText(articleData.title)) {
|
||||
res.json({ target: "title", error: "Not a valid title" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidText(data.introduction)) {
|
||||
if (articleData.introduction && !isValidText(articleData.introduction)) {
|
||||
res.json({ target: "introduction", error: "Not a valid introduction" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.categoryId) {
|
||||
res.json({ target: "category", error: "Category is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
const newArticle: Prisma.ArticleUncheckedUpdateInput = {
|
||||
title: data.title ?? undefined,
|
||||
name: formatTextToUrlName(data.title) ?? undefined,
|
||||
introduction: data.introduction ?? undefined,
|
||||
title: articleData.title ?? undefined,
|
||||
name: articleData.title ? formatTextToUrlName(articleData.title) : undefined,
|
||||
introduction: articleData.introduction ?? undefined,
|
||||
|
||||
categoryId: data.categoryId?.toString() ?? undefined,
|
||||
contentTable: data.contentTable ?? undefined,
|
||||
markdown: data.markdown ?? undefined,
|
||||
imageId: data.imageId?.toString() ?? undefined,
|
||||
categoryId: articleData.categoryId ?? undefined,
|
||||
contentTable: articleData.contentTable ?? undefined,
|
||||
markdown: articleData.markdown ?? undefined,
|
||||
imageUrl: articleData.imageUrl ?? undefined,
|
||||
}
|
||||
console.log(newArticle)
|
||||
await prisma.article.update({ data: newArticle, where: { id: articleId }, include: { category: true } })
|
||||
|
||||
@@ -1,92 +1,48 @@
|
||||
import prisma from "../../../lib/prisma";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Article, Category } from "@prisma/client";
|
||||
import { ResponseError } from "../../../types/responseErrors";
|
||||
import { CreateArticle } from "@/types/api";
|
||||
import prisma, { ArticleWithIncludes } from "../../../lib/prisma";
|
||||
import { formatTextToUrlName } from "../../../utils";
|
||||
import { isValidText } from "../../../validators";
|
||||
import { title } from 'process';
|
||||
import { UpdateArticle } from "../../../types/api";
|
||||
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
console.log("articles index.ts")
|
||||
if (req.method == "GET") { //* GET
|
||||
console.log("get")
|
||||
const categoryName: string = req.query.categoryName?.toString() ?? "";
|
||||
const limit: number = req.query.limit ? Number(req.query.limit) : undefined;
|
||||
const orderBy: string = req.query.orderBy?.toString() ?? "";
|
||||
const category = await prisma.category.findUnique({ where: { name: categoryName } });
|
||||
|
||||
console.log(categoryName, limit, orderBy, category)
|
||||
|
||||
let orderByObj: Prisma.Enumerable<Prisma.ArticleOrderByWithRelationInput>;
|
||||
|
||||
if (orderBy === "recent") {
|
||||
orderByObj = {
|
||||
dateCreated: "desc"
|
||||
}
|
||||
} else if (orderBy === "popularity") {
|
||||
// TODO filter with views
|
||||
}
|
||||
|
||||
await prisma.article
|
||||
.findMany({
|
||||
where: { category: category ?? undefined },
|
||||
include: { category: true },
|
||||
take: limit,
|
||||
orderBy: orderByObj
|
||||
})
|
||||
.then((result: Article[]) => { //! ContentTableEntries not sorted
|
||||
console.log("result", result)
|
||||
if (result !== null) {
|
||||
res.end(JSON.stringify(result));
|
||||
} else {
|
||||
const error: ResponseError = {
|
||||
code: "404",
|
||||
message: "No articles found!",
|
||||
};
|
||||
res.status(404).json(error);
|
||||
}
|
||||
}, (err) => {
|
||||
console.log("reason", err)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("catch", err)
|
||||
const error: ResponseError = {
|
||||
code: "500",
|
||||
message: err,
|
||||
};
|
||||
res.status(500).json(JSON.stringify(error));
|
||||
});
|
||||
if (req.method == "POST") { //* POST
|
||||
console.log("API new article")
|
||||
const articleData: CreateArticle = req.body
|
||||
console.log(articleData)
|
||||
|
||||
|
||||
} else if (req.method == "POST") { //* POST
|
||||
const data: any = req.body;
|
||||
console.log(data)
|
||||
if (!isValidText(data.title)) {
|
||||
res.json({ target: "title", error: "Not a valid title" });
|
||||
if (!isValidText(articleData.title)) {
|
||||
res.status(500).json({ target: "title", error: "Not a valid title" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidText(data.introduction)) {
|
||||
res.json({ target: "introduction", error: "Not a valid introduction" });
|
||||
if (!isValidText(articleData.introduction)) {
|
||||
res.status(500).json({ target: "introduction", error: "Not a valid introduction" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.categoryId) {
|
||||
res.json({ target: "category", error: "Category is required" });
|
||||
if (!articleData.categoryId) {
|
||||
res.status(500).json({ target: "category", error: "Category is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
data.name = formatTextToUrlName(data.title);
|
||||
data.categoryId = data.categoryId.toString();
|
||||
const newArticle: Prisma.ArticleUncheckedCreateInput = {
|
||||
title: articleData.title,
|
||||
name: formatTextToUrlName(articleData.title),
|
||||
introduction: articleData.introduction,
|
||||
categoryId: articleData.categoryId,
|
||||
markdown: articleData.markdown ?? "",
|
||||
contentTable: articleData.contentTable ?? {},
|
||||
imageUrl: articleData.imageUrl ?? ""
|
||||
}
|
||||
|
||||
prisma.article
|
||||
.create({ data: data, include: { category: true } })
|
||||
.create({ data: newArticle, include: { category: true } })
|
||||
.then(
|
||||
(data) => {
|
||||
console.log("success")
|
||||
(data: ArticleWithIncludes) => {
|
||||
res.json({ success: true, data: data });
|
||||
},
|
||||
(errorReason) => {
|
||||
|
||||
@@ -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 { Category } from "@prisma/client";
|
||||
import { ResponseError } from "../../../types/responseErrors";
|
||||
@@ -7,51 +6,29 @@ import { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { formatTextToUrlName } from "../../../utils";
|
||||
import { isValidText } from "../../../validators";
|
||||
import { UpdateCategory } from '../../../types/api';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const categoryId: string = req.query.categoryId.toString() ?? undefined;
|
||||
|
||||
if (req.method == "GET") {
|
||||
await prisma.category
|
||||
.findUnique({ where: { id: categoryId }, include: { svg: true } })
|
||||
.then((result: Category) => {
|
||||
if (result !== null) {
|
||||
res.json(result);
|
||||
} else {
|
||||
const error: ResponseError = {
|
||||
code: "404",
|
||||
message: "No category with this id found!",
|
||||
};
|
||||
res.status(404).json(error);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
const error: ResponseError = {
|
||||
code: "500",
|
||||
message: err,
|
||||
};
|
||||
res.status(500).json(error);
|
||||
});
|
||||
|
||||
} else if (req.method == "PUT") {
|
||||
const data: any = req.body;
|
||||
if (!isValidText(data.title)) {
|
||||
if (req.method == "PUT") {
|
||||
const categoryId: string = req.query.categoryId?.toString() ?? "";
|
||||
const categoryData: UpdateCategory = req.body;
|
||||
|
||||
if (categoryData.title && !isValidText(categoryData.title)) {
|
||||
res.json({ target: "title", error: "Not a valid title" });
|
||||
return;
|
||||
}
|
||||
|
||||
data.name = formatTextToUrlName(data.title);
|
||||
|
||||
console.log(data);
|
||||
const newSvg: Prisma.SvgUncheckedUpdateInput = {
|
||||
viewbox: data.svg.viewbox,
|
||||
path: data.svg.path,
|
||||
viewbox: categoryData.svg?.viewbox ?? undefined,
|
||||
path: categoryData.svg?.path ?? undefined,
|
||||
};
|
||||
|
||||
const newCategory: Prisma.CategoryUncheckedUpdateInput = {
|
||||
title: data.title,
|
||||
name: data.name,
|
||||
color: data.color,
|
||||
title: categoryData.title ?? undefined,
|
||||
name: categoryData.title ? formatTextToUrlName(categoryData.title) : undefined,
|
||||
color: categoryData.color ?? undefined,
|
||||
};
|
||||
|
||||
await prisma.category
|
||||
@@ -62,7 +39,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
})
|
||||
.then(
|
||||
async (categoryData) => {
|
||||
console.log("2");
|
||||
await prisma.svg
|
||||
.update({ data: newSvg, where: { id: categoryData.svg.id } })
|
||||
.then(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import prisma from "../../../lib/prisma";
|
||||
import prisma, { CategoryWithIncludes } from "../../../lib/prisma";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Category, Svg } from "@prisma/client";
|
||||
import { ResponseError } from "../../../types/responseErrors";
|
||||
@@ -7,48 +7,52 @@ import { formatTextToUrlName } from "../../../utils";
|
||||
import { isValidText } from "../../../validators";
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { CreateCategory } from "@/types/api";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method == "GET") { //* GET
|
||||
await prisma.category
|
||||
.findMany({ include: { svg: true } })
|
||||
.then((result: Category[]) => {
|
||||
if (result !== null) {
|
||||
res.json(result);
|
||||
} else {
|
||||
console.log("No categories found");
|
||||
res.json([]);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
res.json([]);
|
||||
});
|
||||
} else if (req.method == "POST") {
|
||||
const data: any = req.body;
|
||||
if (!isValidText(data.title)) {
|
||||
if (req.method == "POST") {
|
||||
console.log("API new category")
|
||||
const categoryData: CreateCategory = req.body;
|
||||
console.log(categoryData)
|
||||
|
||||
if (!isValidText(categoryData.title)) {
|
||||
res.json({ target: "title", error: "Not a valid title" });
|
||||
return;
|
||||
}
|
||||
|
||||
data.name = formatTextToUrlName(data.title);
|
||||
data.svg.viewbox = data.svg.viewbox.length > 1 ? data.svg.viewbox : null;
|
||||
console.log(data);
|
||||
categoryData.svg.viewbox = categoryData.svg.viewbox.length > 1 ? categoryData.svg.viewbox : "";
|
||||
|
||||
const newSvg: Prisma.SvgUncheckedCreateInput = {
|
||||
viewbox: categoryData.svg.viewbox,
|
||||
path: categoryData.svg.path
|
||||
}
|
||||
|
||||
await prisma.svg
|
||||
.create({ data: data.svg })
|
||||
.create({ data: newSvg })
|
||||
.then(
|
||||
async (svgData) => {
|
||||
async (createdSvg: Svg) => {
|
||||
const newCategory: Prisma.CategoryUncheckedCreateInput = {
|
||||
title: categoryData.title,
|
||||
name: formatTextToUrlName(categoryData.title),
|
||||
color: categoryData.color ?? "teal",
|
||||
svgId: createdSvg.id,
|
||||
}
|
||||
|
||||
await prisma.category
|
||||
.create({
|
||||
data: { title: data.title, name: data.name, color: data.name, svgId: svgData.id },
|
||||
include: { svg: true },
|
||||
data: newCategory,
|
||||
include: { svg: true, articles: true },
|
||||
})
|
||||
.then(
|
||||
(data) => {
|
||||
res.json({ success: true, data: data });
|
||||
(createdCategory: CategoryWithIncludes | null) => {
|
||||
if (createdCategory) {
|
||||
res.json({ success: true, data: createdCategory });
|
||||
} else {
|
||||
res.json({ error: true, message: "Could not create category" });
|
||||
}
|
||||
},
|
||||
(errorReason) => {
|
||||
console.log(errorReason)
|
||||
if (errorReason.code === "P2002") {
|
||||
res.json({ target: errorReason.meta.target[0], error: "Already exists" });
|
||||
}
|
||||
|
||||
@@ -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 { ArticleWithIncludes, CategoryWithIncludes, FetchManager } from "../../../manager/fetchManager";
|
||||
import { formatTextToUrlName } from "../../../utils";
|
||||
|
||||
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);
|
||||
import { formatTextToUrlName } from "@/utils";
|
||||
import { Article, Category } from "@prisma/client";
|
||||
import prisma, { CategoryWithIncludes } from "@/lib/prisma";
|
||||
|
||||
export default function CategoryPage({ category }: { category: CategoryWithIncludes | null }) {
|
||||
return (
|
||||
<div className={styles.category}>
|
||||
<h1>{category?.title}</h1>
|
||||
@@ -43,8 +37,8 @@ export default async function CategoryPage({ params }: { params: { categoryName:
|
||||
|
||||
<div className={styles.showcase}>
|
||||
<h2>All articles</h2>
|
||||
{allArticles
|
||||
? Array.from(allArticles).map((a, i) => {
|
||||
{category?.articles
|
||||
? Array.from(category?.articles).map((a: Article, i: number) => {
|
||||
{
|
||||
return (
|
||||
<Link key={i} href={`/articles/${category.name}/${a.name}`}>
|
||||
@@ -59,3 +53,28 @@ export default async function CategoryPage({ params }: { params: { categoryName:
|
||||
</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 { Category } from "@prisma/client";
|
||||
import prisma, { CategoryWithIncludes } from "@/lib/prisma";
|
||||
|
||||
import { FetchManager } from "../../manager/fetchManager";
|
||||
|
||||
export default async function CategoryList() {
|
||||
const categories = await FetchManager.Category.list();
|
||||
export default function CategoryList({ categories }: { categories: CategoryWithIncludes[] }) {
|
||||
return (
|
||||
<div className={styles.categoryList}>
|
||||
<h1>Overview</h1>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.grid}>
|
||||
{categories?.length > 0
|
||||
? categories.map((cat, i) => {
|
||||
? categories.map((cat: CategoryWithIncludes, i: number) => {
|
||||
return (
|
||||
<div key={i} className={styles.linkContainer}>
|
||||
<Link href={`/articles/${cat.name.toLowerCase()}`} style={{ backgroundColor: cat.color }}>
|
||||
@@ -31,3 +30,21 @@ export default async function CategoryList() {
|
||||
</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
|
||||
CREATE TABLE "Article" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"introduction" TEXT NOT NULL DEFAULT '',
|
||||
"imageId" TEXT,
|
||||
"markdown" TEXT NOT NULL,
|
||||
"categoryId" INTEGER,
|
||||
"typeId" INTEGER,
|
||||
"contentTable" JSONB,
|
||||
"categoryId" TEXT NOT NULL,
|
||||
"dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"dateUpdated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
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
|
||||
CREATE TABLE "Category" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"color" TEXT NOT NULL,
|
||||
"svgId" INTEGER NOT NULL,
|
||||
"svgId" TEXT NOT NULL,
|
||||
"dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"dateUpdated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
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
|
||||
CREATE TABLE "Image" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"alt" 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,
|
||||
|
||||
CONSTRAINT "Image_pkey" PRIMARY KEY ("id")
|
||||
@@ -61,8 +42,7 @@ CREATE TABLE "Image" (
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Svg" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"id" TEXT NOT NULL,
|
||||
"path" TEXT NOT NULL DEFAULT '',
|
||||
"viewbox" TEXT NOT NULL DEFAULT '0 0 512 512',
|
||||
|
||||
@@ -81,29 +61,14 @@ CREATE UNIQUE INDEX "Category_name_key" ON "Category"("name");
|
||||
-- CreateIndex
|
||||
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
|
||||
CREATE UNIQUE INDEX "Image_name_key" ON "Image"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Svg_name_key" ON "Svg"("name");
|
||||
-- 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 SET NULL 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;
|
||||
ALTER TABLE "Article" ADD CONSTRAINT "Article_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
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
|
||||
title String @unique
|
||||
introduction String @default("")
|
||||
imageId String?
|
||||
image Image? @relation(fields: [imageId], references: [id])
|
||||
imageUrl String
|
||||
markdown String
|
||||
contentTable Json?
|
||||
categoryId String
|
||||
@@ -29,22 +28,11 @@ model Category {
|
||||
color String
|
||||
svgId String
|
||||
svg Svg @relation(fields: [svgId], references: [id])
|
||||
Article Article[]
|
||||
articles Article[]
|
||||
dateCreated 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 {
|
||||
id String @id @default(uuid())
|
||||
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 {
|
||||
border-top: 2px solid var(--color-danger);
|
||||
border-bottom: 2px solid var(--color-danger);
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -4,23 +4,21 @@
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noImplicitThis": true,
|
||||
"jsx": "preserve",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
"incremental": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ export interface CreateArticle {
|
||||
title: string;
|
||||
markdown: string;
|
||||
introduction: string;
|
||||
categoryId: number;
|
||||
categoryId: string;
|
||||
contentTable: Prisma.JsonArray
|
||||
imageId?: number;
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
export interface UpdateArticle {
|
||||
@@ -15,9 +15,9 @@ export interface UpdateArticle {
|
||||
title?: string;
|
||||
markdown?: string;
|
||||
introduction?: string;
|
||||
categoryId?: number;
|
||||
categoryId?: string;
|
||||
contentTable?: Prisma.JsonArray
|
||||
imageId?: number;
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
export interface CreateCategory {
|
||||
|
||||