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

8
.gitignore vendored
View File

@@ -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

View File

@@ -1,5 +1,5 @@
{
"tabWidth": 2,
"useTabs": false,
"printWidth": 120
"printWidth": 180
}

View File

@@ -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:

View File

@@ -1,3 +0,0 @@
export default async function AdminArticleEditorLayout({ children }) {
return <div>{children}</div>;
}

View File

@@ -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>
);
}

View File

@@ -1,3 +0,0 @@
export default async function AdminCategoriesEditorLayout({ children }) {
return <div>{children}</div>;
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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" />
</>
);
}

View File

@@ -1,3 +0,0 @@
export default function ArticleLayout({ children }) {
return <div>{children}</div>;
}

View File

@@ -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" };
}

View File

@@ -1,7 +0,0 @@
export default async function RootHead() {
return (
<>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</>
);
}

View File

@@ -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>
);
}

View File

@@ -1,3 +0,0 @@
export default function HomePage() {
return <h1>Home</h1>;
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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 (

View File

@@ -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} />;
}

View File

@@ -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>
);
}

View File

@@ -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>
</>

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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 } }>

View File

@@ -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()
}
}
}

View File

@@ -1,8 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
},
reactStrictMode: true,
};
module.exports = nextConfig;

7837
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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
View File

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

14
pages/_document.tsx Normal file
View File

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

View File

@@ -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
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import prisma from "../../../lib/prisma";
import prisma, { CategoryWithIncludes } from "../../../lib/prisma";
import { Prisma } from "@prisma/client";
import { Category, Svg } from "@prisma/client";
import { ResponseError } from "../../../types/responseErrors";
@@ -7,48 +7,52 @@ import { formatTextToUrlName } from "../../../utils";
import { isValidText } from "../../../validators";
import type { NextApiRequest, NextApiResponse } from 'next'
import { CreateCategory } from "@/types/api";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method == "GET") { //* GET
await prisma.category
.findMany({ include: { svg: true } })
.then((result: Category[]) => {
if (result !== null) {
res.json(result);
} else {
console.log("No categories found");
res.json([]);
}
})
.catch((err) => {
console.log(err);
res.json([]);
});
} else if (req.method == "POST") {
const data: any = req.body;
if (!isValidText(data.title)) {
if (req.method == "POST") {
console.log("API new category")
const categoryData: CreateCategory = req.body;
console.log(categoryData)
if (!isValidText(categoryData.title)) {
res.json({ target: "title", error: "Not a valid title" });
return;
}
data.name = formatTextToUrlName(data.title);
data.svg.viewbox = data.svg.viewbox.length > 1 ? data.svg.viewbox : null;
console.log(data);
categoryData.svg.viewbox = categoryData.svg.viewbox.length > 1 ? categoryData.svg.viewbox : "";
const newSvg: Prisma.SvgUncheckedCreateInput = {
viewbox: categoryData.svg.viewbox,
path: categoryData.svg.path
}
await prisma.svg
.create({ data: data.svg })
.create({ data: newSvg })
.then(
async (svgData) => {
async (createdSvg: Svg) => {
const newCategory: Prisma.CategoryUncheckedCreateInput = {
title: categoryData.title,
name: formatTextToUrlName(categoryData.title),
color: categoryData.color ?? "teal",
svgId: createdSvg.id,
}
await prisma.category
.create({
data: { title: data.title, name: data.name, color: data.name, svgId: svgData.id },
include: { svg: true },
data: newCategory,
include: { svg: true, articles: true },
})
.then(
(data) => {
res.json({ success: true, data: data });
(createdCategory: CategoryWithIncludes | null) => {
if (createdCategory) {
res.json({ success: true, data: createdCategory });
} else {
res.json({ error: true, message: "Could not create category" });
}
},
(errorReason) => {
console.log(errorReason)
if (errorReason.code === "P2002") {
res.json({ target: errorReason.meta.target[0], error: "Already exists" });
}

View File

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

View File

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

View File

@@ -0,0 +1,86 @@
import ContentTable from "@/components/ContentTable";
import Sidebar from "@/components/Sidebar";
import styles from "@/styles/modules/Article.module.scss";
import Image from "next/image";
import Markdown from "@/components/Markdown";
import { formatTextToUrlName } from "@/utils";
import prisma, { ArticleWithIncludes, CategoryWithIncludes } from "@/lib/prisma";
import articles from "../..";
//* MAIN
export default function ArticlePage({ article }: { article: ArticleWithIncludes }) {
const dateUpdated: Date = new Date(article?.dateUpdated);
const dateCreated: Date = new Date(article?.dateCreated);
const dateOptions: Intl.DateTimeFormatOptions = { month: "long", day: "numeric", year: "numeric" };
return (
<div className={styles.article}>
<ContentTable contentTableData={article?.contentTable ? article?.contentTable : []} />
<div className={styles.tutorialContent}>
<div className={styles.header}>
<p className={`${styles.dates} text-muted`}>
{`Published on ${dateCreated.toLocaleDateString("en-US", dateOptions)}`}
<br />
{dateUpdated > dateCreated ? `Updated on ${dateUpdated.toLocaleDateString("en-US", dateOptions)}` : ""}
</p>
<h1>{article?.title}</h1>
<div className={styles.tags}>
<a href="#">Docker</a> <a href="#">Setup</a> <a href="#">Ubuntu</a>
</div>
<Image src={""} height={350} width={750} alt={""} quality={100} placeholder="blur" blurDataURL="/images/blur.png" loading="lazy" />
<p>{article?.introduction}</p>
</div>
<Markdown value={article?.markdown ?? ""} />
</div>
<Sidebar />
</div>
);
}
export async function generateStaticParams() {
let articles: ArticleWithIncludes[] = [];
await prisma.article.findMany({ include: { category: true } }).then(
(result: ArticleWithIncludes[]) => {
if (result) {
articles = result;
}
},
(reason: any) => {
console.log(reason);
}
);
return await Promise.all(
articles.map(async (article) => ({
article: article,
}))
);
}
export async function getServerSideProps(context: any) {
const articleName = formatTextToUrlName(context.params.articleName);
let article: ArticleWithIncludes | null = null;
await prisma.article
.findUnique({
where: { name: articleName },
include: { category: true },
})
.then(
(result: ArticleWithIncludes | null) => {
if (result) {
article = JSON.parse(JSON.stringify(result));
}
},
(reason: any) => {
console.log(reason);
}
);
return {
props: { article: article }, // will be passed to the page component as props
};
}

View File

@@ -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
};
}

View File

@@ -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
View File

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

View File

@@ -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;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Article" ALTER COLUMN "contentTable" DROP NOT NULL;

View File

@@ -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;

View 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";

View File

@@ -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("")

View File

@@ -1 +0,0 @@
zrpmayg1wugrgxpm

View File

@@ -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
}]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -1,6 +0,0 @@
Version: 221118-e58fee0fb-Linux-AMD64
Key: 0427e51091ac43eaaa55978febab4e2112bad60c
Secret: be0711bf10f092235e039884f0039400
Session: 54a3619247ffec19480e915f526d69bfd3b82297b3d722fafafe9b60b897e45fe90bc57a500989ca4aa5baef53f1063b5fa5dc45ebeb864f575005341e9186832bd2915f576fb192223ca304c439313aba793c82faae63402518682510396386b8
Status: ce
Serial: zrpmayg1wugrgxpm

View File

@@ -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

View File

@@ -1 +0,0 @@
zrpmayg1wugrgxpm

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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"]
}

View File

@@ -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 {