This commit is contained in:
Janis
2023-01-07 08:52:59 +01:00
parent 6ed3cc22f3
commit dd121030e4
30 changed files with 7745 additions and 8665 deletions

5
.prettierrc Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -0,0 +1,3 @@
//! Using this because publicRuntimeConfig is not implemented in appDir yet
export const apiUrl: string = "http://localhost:3000/api/";

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

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

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

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

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

View File

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

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

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

BIN
public/images/docker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
public/images/test.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

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

View 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%;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -0,0 +1,4 @@
export type ResponseError = {
code: string;
message: string;
};