diff --git a/.env b/.env new file mode 100644 index 0000000..e83cbb8 --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +# Environment variables declared in this file are automatically made available to Prisma. +# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema + +# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. +# See the documentation for all the connection string options: https://pris.ly/d/connection-strings + +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres?schema=explainegy" \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf15673 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +/docker_data/postgres/ + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +.vscode diff --git a/.markdown/test.md b/.markdown/test.md new file mode 100644 index 0000000..7bae116 --- /dev/null +++ b/.markdown/test.md @@ -0,0 +1,230 @@ +--- +__Advertisement :)__ + +- __[pica](https://nodeca.github.io/pica/demo/)__ - high quality and fast image +resize in browser. +- __[babelfish](https://github.com/nodeca/babelfish/)__ - developer friendly +i18n with plurals support and easy syntax. + +You will like those projects! +--- + +## h2 Heading + +### h3 Heading + +#### h4 Heading + +##### h5 Heading + +###### h6 Heading + +## Horizontal Rules + +--- + +--- + +--- + +## Typographic replacements + +Enable typographer option to see result. + +(c) (C) (r) (R) (tm) (TM) (p) (P) +- + +test.. test... test..... test?..... test!.... + +!!!!!! ???? ,, -- --- + +"Smartypants, double quotes" and 'single quotes' + +## Emphasis + +**This is bold text** + +**This is bold text** + +_This is italic text_ + +_This is italic text_ + +~~Strikethrough~~ + +## Blockquotes + +> Blockquotes can also be nested... +> +> > ...by using additional greater-than signs right next to each other... +> > +> > > ...or with spaces between arrows. + +## Lists + +Unordered + +- Create a list by starting a line with `+`, `-`, or `*` +- Sub-lists are made by indenting 2 spaces: + - Marker character change forces new list start: + - Ac tristique libero volutpat at + * Facilisis in pretium nisl aliquet + - Nulla volutpat aliquam velit +- Very easy! + +Ordered + +1. Lorem ipsum dolor sit amet +2. Consectetur adipiscing elit +3. Integer molestie lorem at massa + +4. You can use sequential numbers... +5. ...or keep all the numbers as `1.` + +Start numbering with offset: + +57. foo +1. bar + +## Code + +Inline `code` + +Indented code + + // Some comments + line 1 of code + line 2 of code + line 3 of code + +Block code "fences" + +``` +Sample text here... +``` + +Syntax highlighting + +```js +var foo = function (bar) { + return bar++; +}; + +console.log(foo(5)); +``` + +## Tables + +| Option | Description | +| ------ | ------------------------------------------------------------------------- | +| data | path to data files to supply the data that will be passed into templates. | +| engine | engine to be used for processing templates. Handlebars is the default. | +| ext | extension to be used for dest files. | + +Right aligned columns + +| Option | Description | +| -----: | ------------------------------------------------------------------------: | +| data | path to data files to supply the data that will be passed into templates. | +| engine | engine to be used for processing templates. Handlebars is the default. | +| ext | extension to be used for dest files. | + +## Links + +[link text](http://dev.nodeca.com) + +[link with title](http://nodeca.github.io/pica/demo/ "title text!") + +Autoconverted link https://github.com/nodeca/pica (enable linkify to see) + +## Images + +![Minion](https://octodex.github.com/images/minion.png) +![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat") + +Like links, Images also have a footnote style syntax + +![Alt text][id] + +With a reference later in the document defining the URL location: + +[id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat" + +## Plugins + +The killer feature of `markdown-it` is very effective support of +[syntax plugins](https://www.npmjs.org/browse/keyword/markdown-it-plugin). + +### [Emojies](https://github.com/markdown-it/markdown-it-emoji) + +> Classic markup: :wink: :crush: :cry: :tear: :laughing: :yum: +> +> Shortcuts (emoticons): :-) :-( 8-) ;) + +see [how to change output](https://github.com/markdown-it/markdown-it-emoji#change-output) with twemoji. + +### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup) + +- 19^th^ +- H~2~O + +### [\](https://github.com/markdown-it/markdown-it-ins) + +++Inserted text++ + +### [\](https://github.com/markdown-it/markdown-it-mark) + +==Marked text== + +### [Footnotes](https://github.com/markdown-it/markdown-it-footnote) + +Footnote 1 link[^first]. + +Footnote 2 link[^second]. + +Inline footnote^[Text of inline footnote] definition. + +Duplicated footnote reference[^second]. + +[^first]: Footnote **can have markup** + + and multiple paragraphs. + +[^second]: Footnote text. + +### [Definition lists](https://github.com/markdown-it/markdown-it-deflist) + +Term 1 + +: Definition 1 +with lazy continuation. + +Term 2 with _inline markup_ + +: Definition 2 + + { some code, part of Definition 2 } + + Third paragraph of definition 2. + +_Compact style:_ + +Term 1 +~ Definition 1 + +Term 2 +~ Definition 2a +~ Definition 2b + +### [Abbreviations](https://github.com/markdown-it/markdown-it-abbr) + +This is HTML abbreviation example. + +It converts "HTML", but keep intact partial entries like "xxxHTMLyyy" and so on. + +\*[HTML]: Hyper Text Markup Language + +### [Custom containers](https://github.com/markdown-it/markdown-it-container) + +::: warning +_here be dragons_ +::: diff --git a/.markdown/test2.md b/.markdown/test2.md new file mode 100644 index 0000000..a40cad1 --- /dev/null +++ b/.markdown/test2.md @@ -0,0 +1,9 @@ +## Code + +```js +var foo = function (bar) { + return bar++; +}; + +console.log(foo(5)); +``` diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..054d599 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 2, + "useTabs": false, + "printWidth": 120 +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..c87e042 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. + +[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. + +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. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/app/admin/articles/editor/[articleId]/layout.tsx b/app/admin/articles/editor/[articleId]/layout.tsx new file mode 100644 index 0000000..5ffca51 --- /dev/null +++ b/app/admin/articles/editor/[articleId]/layout.tsx @@ -0,0 +1,3 @@ +export default async function AdminArticleEditorLayout({ children }) { + return
{children}
; +} diff --git a/app/admin/articles/editor/[articleId]/page.tsx b/app/admin/articles/editor/[articleId]/page.tsx new file mode 100644 index 0000000..db12810 --- /dev/null +++ b/app/admin/articles/editor/[articleId]/page.tsx @@ -0,0 +1,295 @@ +"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(""); + const [selectCategoriesOptions, setSelectCategoriesOptions] = useState([]); + const [introduction, setIntroduction] = useState(""); + const [markdown, setMarkdown] = useState(""); + const [contentTable, setContentTable] = useState([]); + + const titleRef = useRef(null); + const categorySelectRef = useRef(null); + const introductionRef = useRef(null); + const markdownTextAreaRef = useRef(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 ( +
+

{params.articleId == "0" ? "Create new article" : "Update article"}

+
+

+ +
+ +
+
+
+ + +
+ + +
+
+
+ + +
+
+ +
+ + +
+
+ +
+ +
+
+ {contentTable?.map((entry: IContentTableEntry, i: number) => { + return ( +
+ { + changeContentTableEntryAnchor(i, e.target.value); + }} + type="text" + placeholder={"Anchor"} + defaultValue={entry.anchor} + /> + { + changeContentTableEntryTitle(i, e.target.value); + }} + type="text" + placeholder={"Title"} + defaultValue={entry.title} + />{" "} + +
+ ); + })} + + +
+ +
+
+
+
+
+ ); +} diff --git a/app/admin/articles/page.tsx b/app/admin/articles/page.tsx new file mode 100644 index 0000000..18b024d --- /dev/null +++ b/app/admin/articles/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import React from "react"; + +export default function AdminArticlesPage() { + return ( +
+

Page to manage articles

+ create new article
+

List of existing articles

+
+ ); +} diff --git a/app/admin/categories/editor/[categoryId]/layout.tsx b/app/admin/categories/editor/[categoryId]/layout.tsx new file mode 100644 index 0000000..5c0796f --- /dev/null +++ b/app/admin/categories/editor/[categoryId]/layout.tsx @@ -0,0 +1,3 @@ +export default async function AdminCategoriesEditorLayout({ children }) { + return
{children}
; +} diff --git a/app/admin/categories/editor/[categoryId]/page.tsx b/app/admin/categories/editor/[categoryId]/page.tsx new file mode 100644 index 0000000..9435edb --- /dev/null +++ b/app/admin/categories/editor/[categoryId]/page.tsx @@ -0,0 +1,180 @@ +"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(""); + const [color, setColor] = useState(""); + const [svgViewbox, setSvgViewbox] = useState(""); + const [svgPath, setSvgPath] = useState(""); + + 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 ( +
+

{params.categoryId == "0" ? "Create new category" : "Update category"}

+
+

+ +
+
+
+ +
+ + +
+
+ +
+ + +
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/app/admin/categories/page.tsx b/app/admin/categories/page.tsx new file mode 100644 index 0000000..3b6f97a --- /dev/null +++ b/app/admin/categories/page.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +export default function AdminCategoriesPage() { + return ( +
+

Page to manage categories

+ create new category
+

List of existing category

+
+ ); +} diff --git a/app/admin/images/Gallery.tsx b/app/admin/images/Gallery.tsx new file mode 100644 index 0000000..92c778a --- /dev/null +++ b/app/admin/images/Gallery.tsx @@ -0,0 +1,26 @@ +"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 ( + {alt} { + window.open(src); + }} + style={style} + /> + ); +}; + +export default function Gallery({ images }: { images: ImageType[] }) { + return ; +} diff --git a/app/admin/images/page.tsx b/app/admin/images/page.tsx new file mode 100644 index 0000000..73319b5 --- /dev/null +++ b/app/admin/images/page.tsx @@ -0,0 +1,24 @@ +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 { + 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 ; +} diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..8400b9b --- /dev/null +++ b/app/admin/page.tsx @@ -0,0 +1,10 @@ +import React from "react"; + +export default function AdminPage() { + return ( +
+

AdminPage to manage explainegy

+ articles categories
+
+ ); +} diff --git a/app/articles/[categoryName]/[articleName]/head.tsx b/app/articles/[categoryName]/[articleName]/head.tsx new file mode 100644 index 0000000..45950b4 --- /dev/null +++ b/app/articles/[categoryName]/[articleName]/head.tsx @@ -0,0 +1,13 @@ +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 ( + <> + {article?.title} + + + ); +} diff --git a/app/articles/[categoryName]/[articleName]/layout.tsx b/app/articles/[categoryName]/[articleName]/layout.tsx new file mode 100644 index 0000000..547720c --- /dev/null +++ b/app/articles/[categoryName]/[articleName]/layout.tsx @@ -0,0 +1,3 @@ +export default function ArticleLayout({ children }) { + return
{children}
; +} diff --git a/app/articles/[categoryName]/[articleName]/page.tsx b/app/articles/[categoryName]/[articleName]/page.tsx new file mode 100644 index 0000000..501b56d --- /dev/null +++ b/app/articles/[categoryName]/[articleName]/page.tsx @@ -0,0 +1,63 @@ +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 ( +
+ +
+
+

+ {`Published on ${dateCreated.toLocaleDateString("en-US", dateOptions)}`} +
+ {dateUpdated > dateCreated ? `Updated on ${dateUpdated.toLocaleDateString("en-US", dateOptions)}` : ""} +

+ +

{article?.title}

+ + {article?.image?.alt +

{article?.introduction}

+
+ +
+ +
+ ); +} + +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 ?? "", + })) + ); +} diff --git a/app/articles/[categoryName]/page.tsx b/app/articles/[categoryName]/page.tsx new file mode 100644 index 0000000..f1d6b2f --- /dev/null +++ b/app/articles/[categoryName]/page.tsx @@ -0,0 +1,97 @@ +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 { + 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 { + 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 { + 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 { + 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 ( +
+

{category?.title}

+
+
+

Most popular articles

+ {popularArticles?.map((a, i) => { + { + return ( + + {a.title} + + ); + } + })} +
+ + {/*
+

Most recent articles

+ {recentArticles?.map((a, i) => { + { + return ( + + {a.name} + + ); + } + })} +
*/} + +
+

All articles

+ {allArticles?.map((a, i) => { + { + return ( + + {a.title} + + ); + } + })} +
+
+
+ ); +} diff --git a/app/articles/page.tsx b/app/articles/page.tsx new file mode 100644 index 0000000..644c9e9 --- /dev/null +++ b/app/articles/page.tsx @@ -0,0 +1,33 @@ +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 ( +
+

Overview

+
+
+ {categories?.length > 0 + ? categories.map((cat, i) => { + return ( +
+ +
+ + + +
+ {cat.title} + +
+ ); + }) + : "We did not find any categories"} +
+
+
+ ); +} diff --git a/app/head.tsx b/app/head.tsx new file mode 100644 index 0000000..34c8670 --- /dev/null +++ b/app/head.tsx @@ -0,0 +1,7 @@ +export default async function RootHead() { + return ( + <> + + + ); +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..1e2e2d5 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,26 @@ +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 ( + + + + +
+ Admin +
+
+
+
{children}
+