This commit is contained in:
Janis
2023-01-29 20:56:59 +01:00
parent 0997e8fdb8
commit d3e5295832
120 changed files with 0 additions and 26885 deletions

View File

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

View File

@@ -1,295 +0,0 @@
"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 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";
type ArticleWithCategory = Prisma.ArticleGetPayload<{ include: { category: true } }>;
export default function AdminArticlesEditorPage({ params }: { params: { articleId: string } }) {
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 titleRef = useRef<HTMLInputElement>(null);
const categorySelectRef = useRef(null);
const introductionRef = useRef<HTMLInputElement>(null);
const markdownTextAreaRef = useRef<HTMLTextAreaElement>(null);
const errorTextRef = useRef(null);
function changeContentTableEntryAnchor(index: number, newAnchor: string) {
setContentTable((prevArray) => {
let newArray = [...prevArray];
newArray[index].anchor = newAnchor;
return newArray;
});
}
function changeContentTableEntryTitle(index: number, newTitle: string) {
setContentTable((prevArray) => {
let newArray = [...prevArray];
newArray[index].anchor = newTitle;
return newArray;
});
}
function removeEntry(index: number) {
let newArray = [...contentTable];
newArray.splice(index, 1);
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 (json.success) {
const newArticle: ArticleWithCategory = json.data;
router.push(urlJoin(`/articles/`, newArticle.category.name, newArticle.name));
}
}
async function updateArticle() {
console.log("Update article");
const payload: UpdateArticle = {
id: params.articleId,
title: titleRef.current.value,
introduction: introductionRef.current.value,
markdown: markdown,
categoryId: Number(categorySelectRef?.current?.getValue()[0]?.value),
contentTable: contentTable,
};
console.log(payload);
await fetch("/api/articles/", {
method: "PUT",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
cache: "no-cache",
body: JSON.stringify(payload),
})
.then(handleResponse)
.catch(console.error);
}
async function createArticle() {
console.log("Create article");
const payload: CreateArticle = {
title: titleRef.current.value,
introduction: introductionRef.current.value,
markdown: markdown,
categoryId: Number(categorySelectRef?.current?.getValue()[0]?.value),
contentTable: contentTable,
};
console.log(payload);
await fetch("/api/articles/", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
cache: "no-cache",
body: JSON.stringify(payload),
})
.then(handleResponse)
.catch(console.error);
}
// 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>
<div className={styles.formControl}>
<p className="text-error" ref={errorTextRef}></p>
<button
type="button"
onClick={() => {
if (params.articleId != "0") {
updateArticle();
} else {
createArticle();
}
}}
>
{params.articleId == "0" ? "Create article" : "Update article"}
</button>
</div>
<div className={styles.form}>
<div className={styles.articleEditor}>
<div className={styles.title}>
<label htmlFor="title">Title</label>
<div className={styles.titleInputs}>
<input
onChange={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>
<div className={styles.category}>
<label htmlFor="title">Category</label>
<Select
ref={categorySelectRef}
className="react-select-container"
classNamePrefix="react-select"
onChange={handleFormChange}
options={selectCategoriesOptions}
/>
</div>
<div className={styles.introduction}>
<label htmlFor="title">Introduction</label>
<input
onChange={handleFormChange}
className={!isValidText(introduction) && introduction ? "error" : ""}
type="text"
name="introduction"
placeholder="Introduction"
ref={introductionRef}
/>
</div>
<div className={styles.markdown}>
<label htmlFor="">Markdown Editor</label>
<div className={styles.markdownEditor}>
<textarea ref={markdownTextAreaRef} onChange={handleFormChange}></textarea>
<Markdown value={markdown} />
</div>
</div>
<div className={styles.contentTable}>
<label htmlFor="">Table of contents</label>
<div className={styles.contentTableEditor}>
<div className={styles.entries}>
{contentTable?.map((entry: IContentTableEntry, i: number) => {
return (
<div key={i}>
<input
onChange={(e) => {
changeContentTableEntryAnchor(i, e.target.value);
}}
type="text"
placeholder={"Anchor"}
defaultValue={entry.anchor}
/>
<input
onChange={(e) => {
changeContentTableEntryTitle(i, e.target.value);
}}
type="text"
placeholder={"Title"}
defaultValue={entry.title}
/>{" "}
<button
onClick={() => {
removeEntry(i);
}}
>
Remove
</button>
</div>
);
})}
<button
onClick={() => {
setContentTable([...contentTable, { title: "", anchor: "" }]);
}}
>
Add
</button>
</div>
<Markdown value={markdown} />
</div>
</div>
</div>
</div>
</div>
);
}

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,180 +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 = {
id: params.categoryId,
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: "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,26 +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,24 +0,0 @@
import React from "react";
import { Image } from "@prisma/client";
import { Image as GalleryImage } from "react-grid-gallery";
import urlJoin from "url-join";
import { apiUrl } from "../../../global";
import Gallery from "./Gallery";
async function getImages(): Promise<GalleryImage[]> {
const result = await fetch(urlJoin(apiUrl, `images`), {
cache: "no-cache",
});
const imageData: Image[] = await result.json();
return imageData.map((img, i) => ({
width: img.width,
height: img.height,
src: img.url,
caption: img.name,
}));
}
export default async function AdminImagesPage() {
return <Gallery images={await getImages()} />;
}

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,63 +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 } }) {
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 ?? "";
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 ?? "",
}))
);
}

View File

@@ -1,97 +0,0 @@
import styles from "../../../styles/modules/Category.module.scss";
import Link from "next/link";
import { apiUrl } from "../../../global";
import { Article, Category } from "@prisma/client";
import urlJoin from "url-join";
async function GetAllArticles(categoryName: string): Promise<any> {
const result: Response = await fetch(urlJoin(apiUrl, `articles?categoryName=${categoryName}`), {
cache: "force-cache",
next: { revalidate: 3600 },
});
return result.json();
}
async function GetPopularArticles(categoryName: string): Promise<any> {
const result: Response = await fetch(
urlJoin(apiUrl, `articles?categoryName=${categoryName}&limit=6&orderBy=popularity`),
{
cache: "force-cache",
next: { revalidate: 3600 },
}
);
return result.json();
}
async function GetRecentArticles(categoryName: string): Promise<any> {
const result: Response = await fetch(
urlJoin(apiUrl, `articles?categoryName=${categoryName}&limit=6&orderBy=recent`),
{
cache: "force-cache",
next: { revalidate: 3600 },
}
);
return result.json();
}
async function GetCategory(categoryName: string): Promise<any> {
const result: Response = await fetch(urlJoin(apiUrl, `categories/name/${categoryName}`), {
cache: "force-cache",
next: { revalidate: 3600 },
});
return result.json();
}
export default async function CategoryPage({ params }: { params: { categoryName: string } }) {
const categoryName = params.categoryName.toLowerCase().replaceAll("%20", " ");
const category: Category = await GetCategory(categoryName);
const allArticles: Article[] = await GetAllArticles(categoryName);
const popularArticles: Article[] = await GetPopularArticles(categoryName);
const recentArticles: Article[] = await GetRecentArticles(categoryName);
return (
<div className={styles.category}>
<h1>{category?.title}</h1>
<div className={styles.content}>
<div className={`${styles.showcase} ${styles.smallShowcase}`}>
<h2>Most popular articles</h2>
{popularArticles?.map((a, i) => {
{
return (
<Link key={i} href={`/articles/${category.name}/${a.name}`}>
{a.title}
</Link>
);
}
})}
</div>
{/* <div className={`${styles.showcase} ${styles.smallShowcase}`}>
<h2>Most recent articles</h2>
{recentArticles?.map((a, i) => {
{
return (
<Link key={i} href={`/articles/${category.name}/${a.name}`}>
{a.name}
</Link>
);
}
})}
</div> */}
<div className={styles.showcase}>
<h2>All articles</h2>
{allArticles?.map((a, i) => {
{
return (
<Link key={i} href={`/articles/${category.name}/${a.name}`}>
{a.title}
</Link>
);
}
})}
</div>
</div>
</div>
);
}

View File

@@ -1,33 +0,0 @@
import styles from "../../styles/modules/CategoryList.module.scss";
import Link from "next/link";
import { FetchManager } from "../../manager/fetchManager";
export default async function CategoryList() {
const categories = await FetchManager.Category.list();
return (
<div className={styles.categoryList}>
<h1>Overview</h1>
<div className={styles.content}>
<div className={styles.grid}>
{categories?.length > 0
? categories.map((cat, i) => {
return (
<div key={i} className={styles.linkContainer}>
<Link href={`/articles/${cat.name.toLowerCase()}`} style={{ backgroundColor: cat.color }}>
<div className={styles.svgContainer}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox={cat?.svg?.viewbox}>
<path d={cat?.svg?.path} />
</svg>
</div>
{cat.title}
</Link>
</div>
);
})
: "We did not find any categories"}
</div>
</div>
</div>
);
}

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,26 +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";
export default async function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html style={{ scrollBehavior: "smooth" }}>
<head></head>
<body className="body">
<div>
<Link href={"/admin"}> Admin</Link>
</div>
<header>
<Nav categories={await FetchManager.Category.list()} />
</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>
);
}