mirror of
https://github.com/DerTyp7/explainegy-nextjs.git
synced 2025-10-29 21:02:13 +01:00
add api
This commit is contained in:
5
.prettierrc
Normal file
5
.prettierrc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
235
app/Nav.tsx
235
app/Nav.tsx
@@ -2,146 +2,127 @@
|
|||||||
import styles from "../styles/modules/Nav.module.scss";
|
import styles from "../styles/modules/Nav.module.scss";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { MutableRefObject, useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import async from "./articles/[categoryName]/[articleName]/head";
|
import { Category } from "@prisma/client";
|
||||||
import ContentTable from "./articles/[categoryName]/[articleName]/ContentTable";
|
import urlJoin from "url-join";
|
||||||
|
import { apiUrl } from "./global";
|
||||||
export type NavCategory = {
|
import { GetStaticProps } from "next";
|
||||||
name: string;
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function switchTheme(theme) {
|
function switchTheme(theme) {
|
||||||
const bodyElement = document.getElementsByTagName("body")[0];
|
const bodyElement = document.getElementsByTagName("body")[0];
|
||||||
|
|
||||||
if (theme == "dark") {
|
if (theme == "dark") {
|
||||||
bodyElement.classList.remove("theme-light");
|
bodyElement.classList.remove("theme-light");
|
||||||
} else {
|
} else {
|
||||||
bodyElement.classList.add("theme-light");
|
bodyElement.classList.add("theme-light");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
const svgElement = document.getElementById("themeSwitchSvg");
|
const svgElement = document.getElementById("themeSwitchSvg");
|
||||||
|
|
||||||
if (localStorage.getItem("theme") == "light") {
|
if (localStorage.getItem("theme") == "light") {
|
||||||
svgElement.style.animationDirection = "normal";
|
svgElement.style.animationDirection = "normal";
|
||||||
svgElement.style.animationName = styles.spinThemeSwitch;
|
svgElement.style.animationName = styles.spinThemeSwitch;
|
||||||
} else {
|
} else {
|
||||||
svgElement.style.animationDirection = "reverse";
|
svgElement.style.animationDirection = "reverse";
|
||||||
svgElement.style.animationName = styles.spinThemeSwitch;
|
svgElement.style.animationName = styles.spinThemeSwitch;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (localStorage.getItem("theme") == "light") {
|
if (localStorage.getItem("theme") == "light") {
|
||||||
localStorage.setItem("theme", "dark");
|
localStorage.setItem("theme", "dark");
|
||||||
switchTheme("dark");
|
switchTheme("dark");
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem("theme", "light");
|
localStorage.setItem("theme", "light");
|
||||||
switchTheme("light");
|
switchTheme("light");
|
||||||
}
|
}
|
||||||
svgElement.style.animationName = "";
|
svgElement.style.animationName = "";
|
||||||
}, 150);
|
}, 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Nav({ categories }: { categories: NavCategory[] }) {
|
export default function Nav({ categories }: { categories: Category[] }) {
|
||||||
const [searchResults, setSearchResults] = useState([]);
|
const [searchResults, setSearchResults] = useState([]);
|
||||||
|
|
||||||
async function handleSearchInput(event) {
|
async function handleSearchInput(event) {
|
||||||
const query = event.target.value;
|
const query = event.target.value;
|
||||||
let result = await fetch(`/api/search?q=${query}`);
|
let result = await fetch(`/api/search?q=${query}`);
|
||||||
let json = await result.json();
|
let json = await result.json();
|
||||||
|
|
||||||
if (json.length == 0 && query.length > 0) {
|
if (json.length == 0 && query.length > 0) {
|
||||||
setSearchResults([{ name: "", title: "No article found..." }]);
|
setSearchResults([{ name: "", title: "No article found..." }]);
|
||||||
} else {
|
} else {
|
||||||
setSearchResults(json);
|
setSearchResults(json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (localStorage.getItem("theme") == "dark") {
|
if (localStorage.getItem("theme") == "dark") {
|
||||||
switchTheme("dark");
|
switchTheme("dark");
|
||||||
} else {
|
} else {
|
||||||
switchTheme("light");
|
switchTheme("light");
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(searchResults);
|
console.log(searchResults);
|
||||||
}, [searchResults]);
|
}, [searchResults]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={styles.nav}>
|
<nav className={styles.nav}>
|
||||||
<div className={styles.containerLeft}>
|
<div className={styles.containerLeft}>
|
||||||
<Image
|
<Image src={"/images/logo.svg"} height={40} width={160} alt="Nav bar logo" />
|
||||||
src={"/images/logo.svg"}
|
<div className={styles.links}>
|
||||||
height={40}
|
<div className={styles.dropDown}>
|
||||||
width={160}
|
<Link href="/articles">Categories</Link>
|
||||||
alt="Nav bar logo"
|
<div className={styles.dropDownContainer}>
|
||||||
/>
|
<div className={styles.content}>
|
||||||
<div className={styles.links}>
|
<Link href={"/articles"}>All</Link>
|
||||||
<div className={styles.dropDown}>
|
{categories?.map((cat, i) => {
|
||||||
<Link href="/articles">Categories</Link>
|
{
|
||||||
<div className={styles.dropDownContainer}>
|
return (
|
||||||
<div className={styles.content}>
|
<Link key={i} href={`/articles/${cat.name.toLowerCase()}`}>
|
||||||
<Link href={"/articles"}>All</Link>
|
{cat.title}
|
||||||
{categories?.map((cat, i) => {
|
</Link>
|
||||||
{
|
);
|
||||||
return (
|
}
|
||||||
<Link
|
})}
|
||||||
key={i}
|
</div>
|
||||||
href={`/articles/${cat.name.toLowerCase()}`}
|
</div>
|
||||||
>
|
</div>
|
||||||
{cat.title}
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
);
|
<div className={styles.containerCenter}>
|
||||||
}
|
<div className={styles.searchBar}>
|
||||||
})}
|
<div className={styles.icon}>
|
||||||
</div>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
</div>
|
<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" />
|
||||||
</div>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<input onInput={handleSearchInput} type="text" name="" id="" />
|
||||||
<div className={styles.containerCenter}>
|
</div>
|
||||||
<div className={styles.searchBar}>
|
<div className={styles.searchResults}>
|
||||||
<div className={styles.icon}>
|
<div className={styles.content}>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
{searchResults.map((s) => {
|
||||||
<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>
|
return <Link href={`/articles/${s.name.toLowerCase()}`}>{s.title}</Link>;
|
||||||
</div>
|
}
|
||||||
<input onInput={handleSearchInput} type="text" name="" id="" />
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.searchResults}>
|
</div>
|
||||||
<div className={styles.content}>
|
</div>
|
||||||
{searchResults.map((s) => {
|
<div className={styles.containerRight}>
|
||||||
{
|
<div
|
||||||
return (
|
className={styles.themeSwitch}
|
||||||
<Link href={`/articles/${s.name.toLowerCase()}`}>
|
onClick={() => {
|
||||||
{s.title}
|
toggleTheme();
|
||||||
</Link>
|
}}
|
||||||
);
|
>
|
||||||
}
|
<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" />
|
||||||
</div>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.containerRight}>
|
</nav>
|
||||||
<div
|
);
|
||||||
className={styles.themeSwitch}
|
|
||||||
onClick={() => {
|
|
||||||
toggleTheme();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
id="themeSwitchSvg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
>
|
|
||||||
<path d="M448 256c0-106-86-192-192-192V448c106 0 192-86 192-192zm64 0c0 141.4-114.6 256-256 256S0 397.4 0 256S114.6 0 256 0S512 114.6 512 256z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,23 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import prisma from "../../../../lib/prisma";
|
import styles from "../../../../styles/modules/ArticleContentTable.module.scss";
|
||||||
import styles from "../../../../styles/modules/TutorialContentTable.module.scss";
|
|
||||||
import { Article, ContentTableEntry } from "@prisma/client";
|
import { Article, ContentTableEntry } from "@prisma/client";
|
||||||
|
|
||||||
export default function ContentTable({
|
export default function ContentTable({ contentTableEntries }: { contentTableEntries: ContentTableEntry[] }) {
|
||||||
contentTableEntries,
|
return (
|
||||||
}: {
|
<div className={styles.articleContentTable}>
|
||||||
contentTableEntries: ContentTableEntry[];
|
<div className={styles.stickyContainer}>
|
||||||
}) {
|
<div className={styles.list}>
|
||||||
return (
|
<h2>Contents</h2>
|
||||||
<div className={styles.tutorialContentTable}>
|
{contentTableEntries?.map((e, i) => {
|
||||||
<div className={styles.stickyContainer}>
|
return (
|
||||||
<div className={styles.list}>
|
<a key={i} href={"#" + e.anchor}>
|
||||||
<h2>Contents</h2>
|
{e.title}
|
||||||
{contentTableEntries?.map((e, i) => {
|
</a>
|
||||||
return (
|
);
|
||||||
<a key={i} href={"#" + e.anchor}>
|
})}
|
||||||
{e.title}
|
</div>
|
||||||
</a>
|
{contentTableEntries?.length < 15 ? <div className={styles.adContainer}>Future advertisement</div> : ""}
|
||||||
);
|
</div>
|
||||||
})}
|
</div>
|
||||||
</div>
|
);
|
||||||
{contentTableEntries?.length < 15 ? (
|
|
||||||
<div className={styles.adContainer}>Future advertisement</div>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,82 +1,93 @@
|
|||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
import ContentTable from "./ContentTable";
|
import ContentTable from "./ContentTable";
|
||||||
import Sidebar from "./Sidebar";
|
import Sidebar from "./Sidebar";
|
||||||
import styles from "../../../../styles/modules/Tutorial.module.scss";
|
import styles from "../../../../styles/modules/Article.module.scss";
|
||||||
import LoadMarkdown from "./LoadMarkdown";
|
import LoadMarkdown from "./LoadMarkdown";
|
||||||
import prisma from "../../../../lib/prisma";
|
|
||||||
import { Article, Category, ContentTableEntry } from "@prisma/client";
|
import { Article, Category, ContentTableEntry } from "@prisma/client";
|
||||||
|
import Image from "next/image";
|
||||||
|
import urlJoin from "url-join";
|
||||||
|
import { apiUrl } from "../../../global";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
export async function GetContentTableEntries(
|
type ArticleWithContentTableEntries = Prisma.ArticleGetPayload<{ include: { contentTableEntries: true } }>;
|
||||||
article: Article
|
type ArticleWithCategory = Prisma.ArticleGetPayload<{ include: { category: true } }>;
|
||||||
): Promise<ContentTableEntry[]> {
|
|
||||||
const entries = await prisma.contentTableEntry.findMany({
|
|
||||||
where: { articleId: article?.id ?? 1 },
|
|
||||||
orderBy: { orderIndex: "asc" },
|
|
||||||
});
|
|
||||||
|
|
||||||
return entries;
|
// export async function GetContentTableEntries(article: Article): Promise<ContentTableEntry[]> {
|
||||||
}
|
// const entries = await prisma.contentTableEntry.findMany({
|
||||||
|
// where: { articleId: article?.id ?? 1 },
|
||||||
|
// orderBy: { orderIndex: "asc" },
|
||||||
|
// });
|
||||||
|
|
||||||
export async function GetArticle(articleName: string) {
|
// return entries;
|
||||||
const article = await prisma.article.findUnique({
|
// }
|
||||||
where: { name: articleName.toLowerCase() ?? "" },
|
|
||||||
});
|
|
||||||
|
|
||||||
return article;
|
export async function GetArticle(articleName: string): Promise<any> {
|
||||||
|
const result: Response = await fetch(urlJoin(apiUrl, `articles/${articleName ?? ""}`), {
|
||||||
|
cache: "force-cache",
|
||||||
|
next: { revalidate: 60 * 10 },
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
function ParseMarkdown(markdown: string): string {
|
function ParseMarkdown(markdown: string): string {
|
||||||
let result = marked.parse(markdown);
|
let result = marked.parse(markdown);
|
||||||
|
return result;
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//* MAIN
|
//* MAIN
|
||||||
export default async function ArticlePage({
|
export default async function ArticlePage({ params }: { params: { articleName: string; categoryName: string } }) {
|
||||||
params,
|
const articleName: string = params.articleName.toLowerCase().replaceAll("%20", " ");
|
||||||
}: {
|
const article: ArticleWithContentTableEntries = await GetArticle(articleName);
|
||||||
params: { articleName: string; categoryName: string };
|
const markdown: string = article?.markdown ?? "";
|
||||||
}) {
|
|
||||||
const articleName: string = params.articleName
|
|
||||||
.toLowerCase()
|
|
||||||
.replaceAll("%20", " ");
|
|
||||||
const article: Article = await GetArticle(articleName);
|
|
||||||
const markdown: string = article?.markdown ?? "";
|
|
||||||
const contentTableEntries: ContentTableEntry[] = await GetContentTableEntries(
|
|
||||||
article
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.tutorial}>
|
<div className={styles.article}>
|
||||||
<ContentTable contentTableEntries={contentTableEntries} />
|
<ContentTable contentTableEntries={article.contentTableEntries} />
|
||||||
<div className={styles.tutorialContent}>
|
<div className={styles.tutorialContent}>
|
||||||
<div className={styles.head}>
|
<div className={styles.header}>
|
||||||
<h1>{article?.title}</h1>
|
<p className="text-muted">Published on January 13, 2022</p>
|
||||||
</div>
|
|
||||||
<div
|
<h1>{article?.title}</h1>
|
||||||
className="markdown"
|
<div className={styles.tags}>
|
||||||
dangerouslySetInnerHTML={{
|
<a href="#">Docker</a> <a href="#">Setup</a> <a href="#">Ubuntu</a>
|
||||||
__html: ParseMarkdown(markdown),
|
</div>
|
||||||
}}
|
<Image
|
||||||
></div>
|
src={"/images/test.jpg"}
|
||||||
<LoadMarkdown />
|
height={350}
|
||||||
</div>
|
width={750}
|
||||||
<Sidebar />
|
alt="Image"
|
||||||
</div>
|
quality={100}
|
||||||
);
|
placeholder="blur"
|
||||||
|
blurDataURL="/images/blur.png"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="markdown"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: ParseMarkdown(markdown),
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<LoadMarkdown />
|
||||||
|
</div>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
const articles = await prisma.article.findMany();
|
const articles: ArticleWithCategory[] = await (
|
||||||
|
await fetch(urlJoin(apiUrl, `articles/`), {
|
||||||
|
cache: "force-cache",
|
||||||
|
next: { revalidate: 60 * 10 },
|
||||||
|
})
|
||||||
|
).json();
|
||||||
|
|
||||||
async function GetCategory(categoryId: number): Promise<Category> {
|
return await Promise.all(
|
||||||
return await prisma.category.findUnique({ where: { id: categoryId } });
|
articles.map(async (article) => ({
|
||||||
}
|
categoryName: article.category?.name ?? "",
|
||||||
|
articleName: article.name ?? "",
|
||||||
return await Promise.all(
|
}))
|
||||||
articles.map(async (article) => ({
|
);
|
||||||
categoryName: (await GetCategory(article.categoryId)).name ?? "",
|
|
||||||
articleName: article.name ?? "",
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,72 @@
|
|||||||
import styles from "../../../styles/modules/Category.module.scss";
|
import styles from "../../../styles/modules/Category.module.scss";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import prisma from "../../../lib/prisma";
|
import { apiUrl } from "../../global";
|
||||||
import { Article, Category } from "@prisma/client";
|
import { Article, Category } from "@prisma/client";
|
||||||
|
import urlJoin from "url-join";
|
||||||
|
|
||||||
export async function GetAllArticles(category: Category): Promise<Article[]> {
|
async function GetAllArticles(categoryName: string): Promise<any> {
|
||||||
return await prisma.article.findMany({ where: { category: category } });
|
const result: Response = await fetch(urlJoin(apiUrl, `articles?categoryName=${categoryName}`), {
|
||||||
|
cache: "force-cache",
|
||||||
|
next: { revalidate: 3600 },
|
||||||
|
});
|
||||||
|
return result.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GetPopularArticles(
|
async function GetPopularArticles(categoryName: string): Promise<any> {
|
||||||
category: Category
|
const result: Response = await fetch(
|
||||||
): Promise<Article[]> {
|
urlJoin(apiUrl, `articles?categoryName=${categoryName}&limit=6&orderBy=popularity`),
|
||||||
return await prisma.article.findMany({
|
{
|
||||||
where: { category: category },
|
cache: "force-cache",
|
||||||
take: 6,
|
next: { revalidate: 3600 },
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
return result.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GetRecentArticles(
|
async function GetRecentArticles(categoryName: string): Promise<any> {
|
||||||
category: Category
|
const result: Response = await fetch(
|
||||||
): Promise<Article[]> {
|
urlJoin(apiUrl, `articles?categoryName=${categoryName}&limit=6&orderBy=recent`),
|
||||||
return await prisma.article.findMany({
|
{
|
||||||
where: { category: category },
|
cache: "force-cache",
|
||||||
take: 6,
|
next: { revalidate: 3600 },
|
||||||
orderBy: { dateCreated: "desc" },
|
}
|
||||||
});
|
);
|
||||||
|
return result.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GetCategory(categoryName: string): Promise<Category> {
|
async function GetCategory(categoryName: string): Promise<any> {
|
||||||
return await prisma.category.findUnique({ where: { name: categoryName } });
|
const result: Response = await fetch(urlJoin(apiUrl, `categories/${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(category);
|
|
||||||
const popularArticles: Article[] = await GetPopularArticles(category);
|
|
||||||
const recentArticles: Article[] = await GetRecentArticles(category);
|
|
||||||
|
|
||||||
return (
|
export default async function CategoryPage({ params }: { params: { categoryName: string } }) {
|
||||||
<div className={styles.category}>
|
const categoryName = params.categoryName.toLowerCase().replaceAll("%20", " ");
|
||||||
<h1>{category?.title}</h1>
|
const category: Category = await GetCategory(categoryName);
|
||||||
<div className={styles.content}>
|
const allArticles: Article[] = await GetAllArticles(categoryName);
|
||||||
<div className={`${styles.showcase} ${styles.smallShowcase}`}>
|
const popularArticles: Article[] = await GetPopularArticles(categoryName);
|
||||||
<h2>Most popular articles</h2>
|
const recentArticles: Article[] = await GetRecentArticles(categoryName);
|
||||||
{popularArticles?.map((a, i) => {
|
console.log(popularArticles);
|
||||||
{
|
return (
|
||||||
return (
|
<div className={styles.category}>
|
||||||
<Link key={i} href={`/articles/${category.name}/${a.name}`}>
|
<h1>{category?.title}</h1>
|
||||||
{a.name}
|
<div className={styles.content}>
|
||||||
</Link>
|
<div className={`${styles.showcase} ${styles.smallShowcase}`}>
|
||||||
);
|
<h2>Most popular articles</h2>
|
||||||
}
|
{popularArticles?.map((a, i) => {
|
||||||
})}
|
{
|
||||||
</div>
|
return (
|
||||||
|
<Link key={i} href={`/articles/${category.name}/${a.name}`}>
|
||||||
|
{a.name}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* <div className={`${styles.showcase} ${styles.smallShowcase}`}>
|
{/* <div className={`${styles.showcase} ${styles.smallShowcase}`}>
|
||||||
<h2>Most recent articles</h2>
|
<h2>Most recent articles</h2>
|
||||||
{recentArticles?.map((a, i) => {
|
{recentArticles?.map((a, i) => {
|
||||||
{
|
{
|
||||||
@@ -70,19 +79,19 @@ export default async function CategoryPage({
|
|||||||
})}
|
})}
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
<div className={styles.showcase}>
|
<div className={styles.showcase}>
|
||||||
<h2>All articles</h2>
|
<h2>All articles</h2>
|
||||||
{allArticles?.map((a, i) => {
|
{allArticles?.map((a, i) => {
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Link key={i} href={`/articles/${category.name}/${a.name}`}>
|
<Link key={i} href={`/articles/${category.name}/${a.name}`}>
|
||||||
{a.name}
|
{a.name}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,45 @@
|
|||||||
import styles from "../../styles/modules/CategoryList.module.scss";
|
import styles from "../../styles/modules/CategoryList.module.scss";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import prisma from "../../lib/prisma";
|
|
||||||
import { Category, Svg, Prisma } from "@prisma/client";
|
import { Category, Svg, Prisma } from "@prisma/client";
|
||||||
import { Suspense } from "react";
|
import urlJoin from "url-join";
|
||||||
import dynamic from "next/dynamic";
|
import { apiUrl } from "../global";
|
||||||
|
|
||||||
type CategoryWithSvg = Prisma.CategoryGetPayload<{ include: { svg: true } }>;
|
type CategoryWithSvg = Prisma.CategoryGetPayload<{ include: { svg: true } }>;
|
||||||
|
|
||||||
export async function GetCategories(): Promise<CategoryWithSvg[]> {
|
export async function GetCategories(): Promise<any> {
|
||||||
return await prisma.category.findMany({ include: { svg: true } });
|
const result: Response = await fetch(urlJoin(apiUrl, `categories`), {
|
||||||
|
cache: "force-cache",
|
||||||
|
next: { revalidate: 3600 },
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function CategoryList() {
|
export default async function CategoryList() {
|
||||||
const categories = await GetCategories();
|
const categories = await GetCategories();
|
||||||
return (
|
return (
|
||||||
<div className={styles.categoryList}>
|
<div className={styles.categoryList}>
|
||||||
<h1>Overview</h1>
|
<h1>Overview</h1>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
{categories?.length > 0
|
{categories?.length > 0
|
||||||
? categories.map((cat, i) => {
|
? categories.map((cat, i) => {
|
||||||
return (
|
return (
|
||||||
<div key={i} className={styles.linkContainer}>
|
<div key={i} className={styles.linkContainer}>
|
||||||
<Link
|
<Link href={`/articles/${cat.name.toLowerCase()}`} style={{ backgroundColor: cat.color }}>
|
||||||
href={`/articles/${cat.name.toLowerCase()}`}
|
<div className={styles.svgContainer}>
|
||||||
style={{ backgroundColor: cat.color }}
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox={cat?.svg?.viewbox}>
|
||||||
>
|
<path d={cat?.svg?.path} />
|
||||||
<div className={styles.svgContainer}>
|
</svg>
|
||||||
<svg
|
</div>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
{cat.title}
|
||||||
viewBox={cat?.svg?.viewbox}
|
</Link>
|
||||||
>
|
</div>
|
||||||
<path d={cat?.svg?.path} />
|
);
|
||||||
</svg>
|
})
|
||||||
</div>
|
: "We did not find any categories"}
|
||||||
{cat.title}
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
|
||||||
: "We did not find any categories"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
3
app/global.ts
Normal file
3
app/global.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
//! Using this because publicRuntimeConfig is not implemented in appDir yet
|
||||||
|
|
||||||
|
export const apiUrl: string = "http://localhost:3000/api/";
|
||||||
@@ -4,32 +4,30 @@ import "../styles/variables.scss";
|
|||||||
import Nav from "./Nav";
|
import Nav from "./Nav";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
import { Category } from "@prisma/client";
|
import { Category } from "@prisma/client";
|
||||||
import prisma from "../lib/prisma";
|
import urlJoin from "url-join";
|
||||||
import { NavCategory } from "./Nav";
|
import { apiUrl } from "./global";
|
||||||
|
|
||||||
export async function GetNavCategories(): Promise<NavCategory[]> {
|
async function getCategories(): Promise<Category[]> {
|
||||||
const result: NavCategory[] = await prisma.category.findMany({
|
const result: Response = await fetch(urlJoin(apiUrl, `categories`), {
|
||||||
select: { name: true, title: true },
|
cache: "force-cache",
|
||||||
});
|
next: { revalidate: 3600 },
|
||||||
return result;
|
});
|
||||||
|
|
||||||
|
return await result.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
children,
|
return (
|
||||||
}: {
|
<html style={{ scrollBehavior: "smooth" }}>
|
||||||
children: React.ReactNode;
|
<head></head>
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<html style={{ scrollBehavior: "smooth" }}>
|
|
||||||
<head></head>
|
|
||||||
|
|
||||||
<body className="body">
|
<body className="body">
|
||||||
<header>
|
<header>
|
||||||
<Nav categories={await GetNavCategories()} />
|
<Nav categories={await getCategories()} />
|
||||||
</header>
|
</header>
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
experimental: {
|
experimental: {
|
||||||
appDir: true,
|
appDir: true,
|
||||||
},
|
},
|
||||||
|
typescript: {
|
||||||
|
// !! WARN !!
|
||||||
|
// Dangerously allow production builds to successfully complete even if your project has type errors.
|
||||||
|
ignoreBuildErrors: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = nextConfig;
|
module.exports = nextConfig;
|
||||||
|
|||||||
15044
package-lock.json
generated
15044
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
78
package.json
78
package.json
@@ -1,42 +1,40 @@
|
|||||||
{
|
{
|
||||||
"name": "explainegy",
|
"name": "explainegy",
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prisma": "prisma generate && prisma db push && prisma studio",
|
"prisma": "prisma generate && prisma db push && prisma studio",
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "prisma generate && prisma migrate deploy && prisma db push && next build",
|
"build": "prisma generate && prisma migrate deploy && next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
},
|
"vercel-build": "prisma generate && prisma db push && next build",
|
||||||
"dependencies": {
|
"prisma:generate": "prisma generate"
|
||||||
"@next/font": "13.0.7",
|
},
|
||||||
"@prisma/client": "^4.8.0",
|
"dependencies": {
|
||||||
"@types/marked": "^4.0.8",
|
"@next/font": "13.0.7",
|
||||||
"@types/pg-promise": "^5.4.3",
|
"@prisma/client": "^4.8.0",
|
||||||
"@types/react": "18.0.26",
|
"@types/express": "^4.17.15",
|
||||||
"@types/react-dom": "18.0.9",
|
"@types/marked": "^4.0.8",
|
||||||
"encoding": "^0.1.13",
|
"@types/react": "18.0.26",
|
||||||
"eslint": "8.30.0",
|
"@types/react-dom": "18.0.9",
|
||||||
"eslint-config-next": "13.0.7",
|
"encoding": "^0.1.13",
|
||||||
"flexsearch": "^0.7.31",
|
"eslint": "8.30.0",
|
||||||
"marked": "^4.2.4",
|
"eslint-config-next": "13.0.7",
|
||||||
"next": "^13.1.1-canary.1",
|
"marked": "^4.2.4",
|
||||||
"node-html-parser": "^6.1.4",
|
"next": "^13.1.2-canary.2",
|
||||||
"pg": "^8.8.0",
|
"prismjs": "^1.29.0",
|
||||||
"pg-promise": "^10.15.4",
|
"react": "18.2.0",
|
||||||
"prismjs": "^1.29.0",
|
"react-code-blocks": "^0.0.9-0",
|
||||||
"react": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-code-blocks": "^0.0.9-0",
|
"reflect-metadata": "^0.1.13",
|
||||||
"react-dom": "18.2.0",
|
"sass": "^1.57.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"typescript": "4.9.4",
|
||||||
"sass": "^1.57.0",
|
"url-join": "^5.0.0"
|
||||||
"typeorm": "^0.3.11",
|
},
|
||||||
"typescript": "4.9.4"
|
"devDependencies": {
|
||||||
},
|
"@types/node": "^18.11.17",
|
||||||
"devDependencies": {
|
"@types/prismjs": "^1.26.0",
|
||||||
"@types/node": "^18.11.17",
|
"prisma": "^4.8.0"
|
||||||
"@types/prismjs": "^1.26.0",
|
}
|
||||||
"prisma": "^4.8.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
31
pages/api/articles/[articleName].ts
Normal file
31
pages/api/articles/[articleName].ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import prisma from "../../../lib/prisma";
|
||||||
|
import { Article } from "@prisma/client";
|
||||||
|
import { ResponseError } from "../../../types/responseErrors";
|
||||||
|
|
||||||
|
export default async function handler(req: Request, res: Response) {
|
||||||
|
res.setHeader("Content-Type", "application/json");
|
||||||
|
|
||||||
|
const articleName: string = req.query.articleName.toString();
|
||||||
|
|
||||||
|
await prisma.article
|
||||||
|
.findUnique({ where: { name: articleName }, include: { category: true, contentTableEntries: true } })
|
||||||
|
.then((result: Article) => {
|
||||||
|
if (result !== null) {
|
||||||
|
res.end(JSON.stringify(result));
|
||||||
|
} else {
|
||||||
|
const error: ResponseError = {
|
||||||
|
code: "404",
|
||||||
|
message: "No article with this name found!",
|
||||||
|
};
|
||||||
|
res.status(404).send(JSON.stringify(error));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const error: ResponseError = {
|
||||||
|
code: "500",
|
||||||
|
message: err,
|
||||||
|
};
|
||||||
|
res.status(500).send(JSON.stringify(error));
|
||||||
|
});
|
||||||
|
}
|
||||||
53
pages/api/articles/index.ts
Normal file
53
pages/api/articles/index.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import prisma from "../../../lib/prisma";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { Article, Category } from "@prisma/client";
|
||||||
|
import { ResponseError } from "../../../types/responseErrors";
|
||||||
|
|
||||||
|
export default async function handler(req: Request, res: Response) {
|
||||||
|
res.setHeader("Content-Type", "application/json");
|
||||||
|
|
||||||
|
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 } });
|
||||||
|
|
||||||
|
let orderByObj: Prisma.Enumerable<Prisma.ArticleOrderByWithRelationInput>;
|
||||||
|
|
||||||
|
if (orderBy === "recent") {
|
||||||
|
orderByObj = {
|
||||||
|
dateCreated: "desc"
|
||||||
|
}
|
||||||
|
} else if (orderBy === "popularity") {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
await prisma.article
|
||||||
|
.findMany({
|
||||||
|
where: { category: categoryName.length > 0 ? category : undefined },
|
||||||
|
include: { category: true, contentTableEntries: true },
|
||||||
|
take: limit,
|
||||||
|
orderBy: orderByObj
|
||||||
|
})
|
||||||
|
.then((result: Article[]) => {
|
||||||
|
if (result !== null) {
|
||||||
|
|
||||||
|
res.end(JSON.stringify(result));
|
||||||
|
} else {
|
||||||
|
const error: ResponseError = {
|
||||||
|
code: "404",
|
||||||
|
message: "No articles found!",
|
||||||
|
};
|
||||||
|
res.status(404).send(JSON.stringify(error));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const error: ResponseError = {
|
||||||
|
code: "500",
|
||||||
|
message: err,
|
||||||
|
};
|
||||||
|
res.status(500).send(JSON.stringify(error));
|
||||||
|
});
|
||||||
|
}
|
||||||
31
pages/api/categories/[categoryName].tsx
Normal file
31
pages/api/categories/[categoryName].tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import prisma from "../../../lib/prisma";
|
||||||
|
import { Category } from "@prisma/client";
|
||||||
|
import { ResponseError } from "../../../types/responseErrors";
|
||||||
|
|
||||||
|
export default async function handler(req: Request, res: Response) {
|
||||||
|
res.setHeader("Content-Type", "application/json");
|
||||||
|
|
||||||
|
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.end(JSON.stringify(result));
|
||||||
|
} else {
|
||||||
|
const error: ResponseError = {
|
||||||
|
code: "404",
|
||||||
|
message: "No category with this name found!",
|
||||||
|
};
|
||||||
|
res.status(404).send(JSON.stringify(error));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const error: ResponseError = {
|
||||||
|
code: "500",
|
||||||
|
message: err,
|
||||||
|
};
|
||||||
|
res.status(500).send(JSON.stringify(error));
|
||||||
|
});
|
||||||
|
}
|
||||||
29
pages/api/categories/index.tsx
Normal file
29
pages/api/categories/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import prisma from "../../../lib/prisma";
|
||||||
|
import { Category } from "@prisma/client";
|
||||||
|
import { ResponseError } from "../../../types/responseErrors";
|
||||||
|
|
||||||
|
export default async function handler(req: Request, res: Response) {
|
||||||
|
res.setHeader("Content-Type", "application/json");
|
||||||
|
|
||||||
|
await prisma.category
|
||||||
|
.findMany({ include: { svg: true } })
|
||||||
|
.then((result: Category[]) => {
|
||||||
|
if (result !== null) {
|
||||||
|
res.end(JSON.stringify(result));
|
||||||
|
} else {
|
||||||
|
const error: ResponseError = {
|
||||||
|
code: "404",
|
||||||
|
message: "No categories found!",
|
||||||
|
};
|
||||||
|
res.status(404).send(JSON.stringify(error));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const error: ResponseError = {
|
||||||
|
code: "500",
|
||||||
|
message: err,
|
||||||
|
};
|
||||||
|
res.status(500).send(JSON.stringify(error));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,31 +1,31 @@
|
|||||||
import prisma from "../../lib/prisma";
|
import prisma from "../../lib/prisma";
|
||||||
|
|
||||||
export default async function search(req, res) {
|
export default async function handler(req, res) {
|
||||||
res.setHeader("Content-Type", "application/json");
|
res.setHeader("Content-Type", "application/json");
|
||||||
let query: string = req.query?.q ?? "";
|
let query: string = req.query?.q ?? "";
|
||||||
|
|
||||||
query = query.toLowerCase().replaceAll("%20", "");
|
query = query.toLowerCase().replaceAll("%20", "");
|
||||||
query = query.toLowerCase().replaceAll(" ", "");
|
query = query.toLowerCase().replaceAll(" ", "");
|
||||||
|
|
||||||
if (query.length > 1) {
|
if (query.length > 0) {
|
||||||
const articles = await prisma.article.findMany({
|
const articles = await prisma.article.findMany({
|
||||||
select: { title: true, name: true },
|
select: { title: true, name: true },
|
||||||
take: 5,
|
take: 5,
|
||||||
}); //TODO order by most viewed
|
}); //TODO order by most viewed
|
||||||
|
|
||||||
let result = [];
|
let result = [];
|
||||||
|
|
||||||
articles.forEach((a) => {
|
articles.forEach((a) => {
|
||||||
let title = a.title.toLowerCase().replaceAll(" ", "");
|
let title = a.title.toLowerCase().replaceAll(" ", "");
|
||||||
title = title.toLowerCase().replaceAll("%20", "");
|
title = title.toLowerCase().replaceAll("%20", "");
|
||||||
|
|
||||||
if (title.includes(query)) {
|
if (title.includes(query)) {
|
||||||
result.push(a);
|
result.push(a);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
res.end(JSON.stringify(result));
|
res.end(JSON.stringify(result));
|
||||||
} else {
|
} else {
|
||||||
res.end(JSON.stringify([]));
|
res.end(JSON.stringify([]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
109
prisma/migrations/20230107035654_init/migration.sql
Normal file
109
prisma/migrations/20230107035654_init/migration.sql
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Article" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"markdown" TEXT NOT NULL,
|
||||||
|
"categoryId" INTEGER,
|
||||||
|
"typeId" INTEGER,
|
||||||
|
"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,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"color" TEXT NOT NULL,
|
||||||
|
"svgId" INTEGER 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,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"url" TEXT NOT NULL DEFAULT '',
|
||||||
|
"dateCreated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "Image_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Svg" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"path" TEXT NOT NULL DEFAULT '',
|
||||||
|
"viewbox" TEXT NOT NULL DEFAULT '0 0 512 512',
|
||||||
|
|
||||||
|
CONSTRAINT "Svg_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Article_name_key" ON "Article"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Article_title_key" ON "Article"("title");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
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_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;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Category" ADD CONSTRAINT "Category_svgId_fkey" FOREIGN KEY ("svgId") REFERENCES "Svg"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
||||||
@@ -53,6 +53,13 @@ model ArticleType {
|
|||||||
dateUpdated DateTime @default(now())
|
dateUpdated DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Image {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String @unique
|
||||||
|
url String @default("")
|
||||||
|
dateCreated DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
model Svg {
|
model Svg {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String @unique
|
name String @unique
|
||||||
|
|||||||
BIN
public/images/blur.png
Normal file
BIN
public/images/blur.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 549 B |
BIN
public/images/docker.png
Normal file
BIN
public/images/docker.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
public/images/test.jpg
Normal file
BIN
public/images/test.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
37
styles/modules/Article.module.scss
Normal file
37
styles/modules/Article.module.scss
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
@import "../variables.scss";
|
||||||
|
.article {
|
||||||
|
display: grid;
|
||||||
|
gap: 70px;
|
||||||
|
grid-template-columns: $tutorial-content-table-width minmax(0px, 1fr) $tutorial-sidebar-width;
|
||||||
|
margin: 0px auto;
|
||||||
|
max-width: 1800px;
|
||||||
|
padding: 0px 24px;
|
||||||
|
|
||||||
|
@media (max-width: $tutorial-breakpoint-1) {
|
||||||
|
grid-template-columns: $tutorial-content-table-width 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $tutorial-breakpoint-2) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0px 10px 0px;
|
||||||
|
gap: 10px 0px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: min(100%, 750px);
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 15/7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorialContent {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
styles/modules/ArticleContentTable.module.scss
Normal file
29
styles/modules/ArticleContentTable.module.scss
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
@import "../variables.scss";
|
||||||
|
|
||||||
|
.articleContentTable {
|
||||||
|
@media (max-width: $tutorial-breakpoint-2) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.stickyContainer {
|
||||||
|
position: sticky;
|
||||||
|
top: $tutorial-grid-sticky-top;
|
||||||
|
|
||||||
|
.list {
|
||||||
|
align-items: flex-start;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
row-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adContainer {
|
||||||
|
background-color: #ff00003e;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
@import "../variables.scss";
|
@import "../variables.scss";
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
background-color: var(--color-background-nav);
|
background-color: var(--color-background-nav);
|
||||||
height: $nav-height-inital;
|
height: $nav-height-inital;
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
@import "../variables.scss";
|
|
||||||
.tutorial {
|
|
||||||
display: grid;
|
|
||||||
gap: 70px;
|
|
||||||
grid-template-columns: $tutorial-content-table-width minmax(0px, 1fr) $tutorial-sidebar-width;
|
|
||||||
margin: 0px auto;
|
|
||||||
max-width: 1800px;
|
|
||||||
padding: 0px 24px;
|
|
||||||
|
|
||||||
@media (max-width: $tutorial-breakpoint-1) {
|
|
||||||
grid-template-columns: $tutorial-content-table-width 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: $tutorial-breakpoint-2) {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tutorialContent {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
@import "../variables.scss";
|
|
||||||
|
|
||||||
.tutorialContentTable {
|
|
||||||
@media (max-width: $tutorial-breakpoint-2) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.stickyContainer {
|
|
||||||
position: sticky;
|
|
||||||
top: $tutorial-grid-sticky-top;
|
|
||||||
|
|
||||||
.list {
|
|
||||||
align-items: flex-start;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
row-gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.adContainer {
|
|
||||||
background-color: #ff00003e;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,66 +7,72 @@
|
|||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
font-family: "Roboto", sans-serif;
|
font-family: "Roboto", sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
color: var(--color-font);
|
color: var(--color-font);
|
||||||
font-size: $font-size-default;
|
font-size: $font-size-default;
|
||||||
letter-spacing: $font-letter-spacing-default;
|
letter-spacing: $font-letter-spacing-default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Headings */
|
/* Headings */
|
||||||
h1 {
|
h1 {
|
||||||
font-size: calc($font-size-default + $font-size-headline-step * 6);
|
font-size: calc($font-size-default + $font-size-headline-step * 6);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
letter-spacing: $font-letter-spacing-headline-large;
|
letter-spacing: $font-letter-spacing-headline-large;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
// 5 skipped on purpose
|
// 5 skipped on purpose
|
||||||
font-size: calc($font-size-default + $font-size-headline-step * 4);
|
font-size: calc($font-size-default + $font-size-headline-step * 4);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
letter-spacing: $font-letter-spacing-headline-medium;
|
letter-spacing: $font-letter-spacing-headline-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: calc($font-size-default + $font-size-headline-step * 3);
|
font-size: calc($font-size-default + $font-size-headline-step * 3);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
letter-spacing: $font-letter-spacing-headline-small;
|
letter-spacing: $font-letter-spacing-headline-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
font-size: calc($font-size-default + $font-size-headline-step * 2);
|
font-size: calc($font-size-default + $font-size-headline-step * 2);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
letter-spacing: $font-letter-spacing-headline-small;
|
letter-spacing: $font-letter-spacing-headline-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
font-size: calc($font-size-default + $font-size-headline-step * 1);
|
font-size: calc($font-size-default + $font-size-headline-step * 1);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
letter-spacing: $font-letter-spacing-headline-small;
|
letter-spacing: $font-letter-spacing-headline-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
h6 {
|
h6 {
|
||||||
font-size: $font-size-default;
|
font-size: $font-size-default;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
letter-spacing: 1.25px;
|
letter-spacing: 1.25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* General Texts */
|
/* General Texts */
|
||||||
a {
|
a {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--color-font-link);
|
color: var(--color-font-link);
|
||||||
transition: color 50ms linear;
|
transition: color 50ms linear;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
color: var(--color-font-link-hover) !important;
|
color: var(--color-font-link-hover) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:visited {
|
&:visited {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--color-font-link);
|
color: var(--color-font-link);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: var(--color-font-muted);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,60 +3,61 @@
|
|||||||
// Using CSS variables allows us to change the values at runtime
|
// Using CSS variables allows us to change the values at runtime
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/*! By default colors are in DARK mode */
|
/*! By default colors are in DARK mode */
|
||||||
/* Colors: General */
|
/* Colors: General */
|
||||||
--color-background-body: #181a1b;
|
--color-background-body: #181a1b;
|
||||||
--color-font: #ffffff;
|
--color-font: #ffffff;
|
||||||
--color-shadow-nav: #00000033;
|
--color-font-muted: #929292;
|
||||||
|
--color-shadow-nav: #00000033;
|
||||||
|
|
||||||
--color-overlay-mix: var(--color-font);
|
--color-overlay-mix: var(--color-font);
|
||||||
--color-background-nav: transparent;
|
--color-background-nav: transparent;
|
||||||
--color-svg-nav: rgb(191, 191, 191);
|
--color-svg-nav: #bfbfbf;
|
||||||
|
|
||||||
--color-background-card: rgba(123, 123, 123, 0.13);
|
--color-background-card: #7b7b7b21;
|
||||||
--color-background-dropdown: var(--color-background-body);
|
--color-background-dropdown: var(--color-background-body);
|
||||||
--color-accent: #2294ff;
|
--color-accent: #2294ff;
|
||||||
--color-font-link: var(--color-accent);
|
--color-font-link: var(--color-accent);
|
||||||
|
|
||||||
--color-font-link-hover: #5caffc;
|
--color-font-link-hover: #5caffc;
|
||||||
|
|
||||||
/* Colors: Markdown */
|
/* Colors: Markdown */
|
||||||
--md-color-font: rgb(220, 217, 217);
|
--md-color-font: #dcd9d9;
|
||||||
--md-color-headline: rgb(229, 228, 228);
|
--md-color-headline: #e5e4e4;
|
||||||
--md-color-hr: rgba(125, 125, 125, 0.481);
|
--md-color-hr: #7d7d7d7b;
|
||||||
|
|
||||||
--md-color-table-col-even-background: #3b556f;
|
--md-color-table-col-even-background: #3b556f;
|
||||||
--md-color-table-col-odd-background: #2f4459;
|
--md-color-table-col-odd-background: #2f4459;
|
||||||
--md-color-table-row-even-background: rgba(52, 52, 52, 0.174);
|
--md-color-table-row-even-background: #3434342c;
|
||||||
--md-color-table-row-odd-background: transparent;
|
--md-color-table-row-odd-background: transparent;
|
||||||
--md-color-blockquote-border: var(--color-accent);
|
--md-color-blockquote-border: var(--color-accent);
|
||||||
--md-color-blockquote-background: rgba(52, 52, 52, 0.2);
|
--md-color-blockquote-background: #34343433;
|
||||||
|
|
||||||
.theme-light {
|
.theme-light {
|
||||||
--color-background-body: #ffffff;
|
--color-background-body: #ffffff;
|
||||||
--color-font: #000000;
|
--color-font: #000000;
|
||||||
--color-shadow-nav: #000c2b0d;
|
--color-shadow-nav: #000c2b0d;
|
||||||
|
|
||||||
--color-overlay-mix: var(--color-font);
|
--color-overlay-mix: var(--color-font);
|
||||||
--color-background-nav: transparent;
|
--color-background-nav: transparent;
|
||||||
--color-svg-nav: rgb(54, 54, 54);
|
--color-svg-nav: #363636;
|
||||||
|
|
||||||
--color-background-card: rgba(171, 170, 170, 0.13);
|
--color-background-card: rgba(171, 170, 170, 0.13);
|
||||||
--color-background-dropdown: var(--color-background-body);
|
--color-background-dropdown: var(--color-background-body);
|
||||||
--color-accent: #2294ff;
|
--color-accent: #2294ff;
|
||||||
--color-font-link: var(--color-accent);
|
--color-font-link: var(--color-accent);
|
||||||
--color-font-link-hover: #0966be;
|
--color-font-link-hover: #0966be;
|
||||||
|
|
||||||
/* Colors: Markdown */
|
/* Colors: Markdown */
|
||||||
--md-color-font: rgb(33, 33, 33);
|
--md-color-font: rgb(33, 33, 33);
|
||||||
--md-color-headline: rgb(22, 22, 22);
|
--md-color-headline: rgb(22, 22, 22);
|
||||||
--md-color-hr: rgba(145, 145, 145, 0.481);
|
--md-color-hr: rgba(145, 145, 145, 0.481);
|
||||||
|
|
||||||
--md-color-table-col-even-background: #3b556f;
|
--md-color-table-col-even-background: #3b556f;
|
||||||
--md-color-table-col-odd-background: #2f4459;
|
--md-color-table-col-odd-background: #2f4459;
|
||||||
--md-color-table-row-even-background: rgba(150, 148, 148, 0.174);
|
--md-color-table-row-even-background: rgba(150, 148, 148, 0.174);
|
||||||
--md-color-table-row-odd-background: transparent;
|
--md-color-table-row-odd-background: transparent;
|
||||||
--md-color-blockquote-border: var(--color-accent);
|
--md-color-blockquote-border: var(--color-accent);
|
||||||
--md-color-blockquote-background: rgba(176, 175, 175, 0.2);
|
--md-color-blockquote-background: rgba(176, 175, 175, 0.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
types/responseErrors.tsx
Normal file
4
types/responseErrors.tsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type ResponseError = {
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user