Add project page
This commit is contained in:
parent
85acc1bdac
commit
f30332bc5e
@ -1,11 +1,21 @@
|
|||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
import styles from "../../styles/Blog/Card.module.scss";
|
import styles from "../../styles/Blog/Card.module.scss";
|
||||||
|
|
||||||
const ProjectCard: NextPage<{ title: string, description: string }> = ({ title, description}) => {
|
interface IContentCard {
|
||||||
return <div className={styles.card}>
|
name: string;
|
||||||
<h3 className={styles.title}>{title}</h3>
|
title: string;
|
||||||
<p className={styles.description}>{description}</p>
|
description: string;
|
||||||
</div>;
|
type: "project" | "diary";
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContentCard: NextPage<IContentCard> = (content: IContentCard) => {
|
||||||
|
return <Link href={`/blog/${content.type}/${content.name}`} passHref>
|
||||||
|
<div className={styles.card}>
|
||||||
|
<h3 className={styles.title}>{content.title}</h3>
|
||||||
|
<p className={styles.description}>{content.description}</p>
|
||||||
|
</div>
|
||||||
|
</Link>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectCard;
|
export default ContentCard;
|
@ -5,7 +5,7 @@ import styles from "../../styles/Terminal/ProjectModal.module.css";
|
|||||||
import asciidocStyles from "../../styles/Terminal/customAsciidoc.module.scss";
|
import asciidocStyles from "../../styles/Terminal/customAsciidoc.module.scss";
|
||||||
import type { Project, Diary } from "../../lib/content/types";
|
import type { Project, Diary } from "../../lib/content/types";
|
||||||
import { useCommands } from "../../lib/commands/ContextProvider";
|
import { useCommands } from "../../lib/commands/ContextProvider";
|
||||||
import { generateContent, projectEmpty } from "../../lib/content/generate";
|
import { generateContent, projectEmpty } from "../../lib/content/generateBrowser";
|
||||||
import { useModalFunctions } from "./contexts/ModalFunctions";
|
import { useModalFunctions } from "./contexts/ModalFunctions";
|
||||||
import Spinner from "../Spinner";
|
import Spinner from "../Spinner";
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
@ -24,7 +24,7 @@ services:
|
|||||||
- "traefik.http.routers.website-dev-secure.service=website-dev"
|
- "traefik.http.routers.website-dev-secure.service=website-dev"
|
||||||
- "traefik.http.services.website-dev.loadbalancer.server.port=3000"
|
- "traefik.http.services.website-dev.loadbalancer.server.port=3000"
|
||||||
environment:
|
environment:
|
||||||
- BASE_URL=https://dev.c0ntroller.de
|
- IS_DEV_ENV=true
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
traefik:
|
traefik:
|
||||||
|
@ -24,7 +24,7 @@ services:
|
|||||||
- "traefik.http.routers.website-stable-secure.service=website-stable"
|
- "traefik.http.routers.website-stable-secure.service=website-stable"
|
||||||
- "traefik.http.services.website-stable.loadbalancer.server.port=3000"
|
- "traefik.http.services.website-stable.loadbalancer.server.port=3000"
|
||||||
environment:
|
environment:
|
||||||
- BASE_URL=https://c0ntroller.de
|
- IS_DEV_ENV=true
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
traefik:
|
traefik:
|
||||||
|
95
lib/content/generateBackend.ts
Normal file
95
lib/content/generateBackend.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// This file is used to generate the HTML for the projects and diaries in the backend.
|
||||||
|
// We can use fs and stuff here.
|
||||||
|
|
||||||
|
import { readdirSync } from "fs";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { resolve } from "path";
|
||||||
|
import type { Project, Diary } from "./types";
|
||||||
|
import asciidoctor from "asciidoctor";
|
||||||
|
|
||||||
|
export const projectEmpty = "<div>Kein Projekt ausgewählt.</div>";
|
||||||
|
const projectNotFoundHtml = `<div class="${"error"}">Sorry! There is no data for this project. Please check back later to see if that changed!</div>`;
|
||||||
|
const projectServerErrorHtml = `<div class="${"error"}">Sorry! A server error happend when the project data was fetched!</div>`;
|
||||||
|
|
||||||
|
const ad = asciidoctor();
|
||||||
|
|
||||||
|
// No error catching here as we are screwed if this fails
|
||||||
|
const projectPath = resolve("./public", "content", "projects");
|
||||||
|
const diaryPath = resolve("./public", "content", "diaries");
|
||||||
|
const projectFiles = readdirSync(projectPath, { withFileTypes: true }).filter((f) => f.isFile() && f.name.endsWith(".adoc"));
|
||||||
|
// As we need the diaries too, no filter here
|
||||||
|
const diaryFiles = readdirSync(diaryPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
export async function generateContent(content: Project|Diary, selectedPage?: number): Promise<string> {
|
||||||
|
if(!content) return projectEmpty;
|
||||||
|
switch (content.type) {
|
||||||
|
case "project": return await generateProjectHTML(content);
|
||||||
|
case "diary": return await generateDiaryHTML(content, selectedPage);
|
||||||
|
default: return projectNotFoundHtml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateProjectHTML(project: Project): Promise<string> {
|
||||||
|
// First we test if the file exist
|
||||||
|
if(!projectFiles.find((f) => f.name === `${project.name}.adoc`)) return projectNotFoundHtml;
|
||||||
|
|
||||||
|
// Resolve the path
|
||||||
|
const path = resolve(projectPath, `${project.name}.adoc`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read the file
|
||||||
|
const rawAd = await readFile(path, { encoding: "utf-8" });
|
||||||
|
|
||||||
|
// Correct the paths so that the images are loaded correctly
|
||||||
|
const pathsCorrected = rawAd.replace(/(image[:]+)(.*\.[a-zA-Z]+)\[/g, "$1/content/projects/$2[");
|
||||||
|
const adDoc = ad.load(pathsCorrected, { attributes: { showtitle: true } });
|
||||||
|
|
||||||
|
// Return and add the footer
|
||||||
|
return `${adDoc.convert(adDoc).toString()}
|
||||||
|
<hr>
|
||||||
|
<div id="footer">
|
||||||
|
<div id="footer-text">
|
||||||
|
Last updated: ${new Date(adDoc.getAttribute("docdatetime")).toLocaleString()} | <a href="https://git.c0ntroller.de/c0ntroller/frontpage-content/src/branch/${process.env.IS_DEV ? "dev" : "senpai"}/projects/${project.name}.adoc" target="_blank">Document source</a>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
} catch (e) {
|
||||||
|
// Something gone wrong
|
||||||
|
console.error(e);
|
||||||
|
return projectServerErrorHtml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateDiaryHTML(diary: Diary, selectedPage?: number): Promise<string> {
|
||||||
|
// First we test if the file exist
|
||||||
|
if(!diaryFiles.find((f) => f.isFile() && f.name === `${diary.name}.adoc`)) return projectNotFoundHtml;
|
||||||
|
|
||||||
|
// First we need the page number and the path to load
|
||||||
|
const page: number = Number.parseInt(selectedPage?.toString() || "0") - 1;
|
||||||
|
// If the page number is not -1, a directory must exist
|
||||||
|
if (page !== -1 && !diaryFiles.find((f) => f.isDirectory() && f.name === diary.name)) return projectNotFoundHtml;
|
||||||
|
|
||||||
|
// Next we load the correct path
|
||||||
|
const path = page === -1 ? resolve(diaryPath, `${diary.name}.adoc`) : resolve(diaryPath, diary.name, `${diary.entries[page].filename}.adoc`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read the file
|
||||||
|
const rawAd = await readFile(path, { encoding: "utf-8" });
|
||||||
|
|
||||||
|
// Correct the paths so that the images are loaded correctly
|
||||||
|
const pathsCorrected = rawAd.replace(/(image[:]{1,2})(.*\.[a-zA-Z]+)\[/g, "$1/content/diaries/$2[");
|
||||||
|
const adDoc = ad.load(pathsCorrected, { attributes: { showtitle: true } });
|
||||||
|
const gitfile = page === -1 ? `${diary.name}.adoc` : `${diary.name}/${diary.entries[page].filename}.adoc`;
|
||||||
|
// Return and add the footer
|
||||||
|
return `${adDoc.convert(adDoc).toString()}
|
||||||
|
<hr>
|
||||||
|
<div id="footer">
|
||||||
|
<div id="footer-text">
|
||||||
|
Last updated: ${new Date(adDoc.getAttribute("docdatetime")).toLocaleString()} | <a href="https://git.c0ntroller.de/c0ntroller/frontpage-content/src/branch/${process.env.IS_DEV ? "dev" : "senpai"}/diaries/${gitfile}" target="_blank">Document source</a>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
} catch (e) {
|
||||||
|
// Something gone wrong
|
||||||
|
console.error(e);
|
||||||
|
return projectServerErrorHtml;
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
|
// This file is used for the generation of the static HTML files in the frontend.
|
||||||
|
// It is used by the terminal.
|
||||||
|
|
||||||
import type { Project, Diary } from "./types";
|
import type { Project, Diary } from "./types";
|
||||||
import asciidoctor from "asciidoctor";
|
import asciidoctor from "asciidoctor";
|
||||||
|
|
||||||
@ -7,6 +10,8 @@ const projectServerErrorHtml = `<div class="${"error"}">Sorry! A server error ha
|
|||||||
|
|
||||||
const ad = asciidoctor();
|
const ad = asciidoctor();
|
||||||
|
|
||||||
|
const isDev = window?.location?.host.startsWith("dev") || false;
|
||||||
|
|
||||||
export async function generateContent(content: Project|Diary, selectedPage?: number): Promise<string> {
|
export async function generateContent(content: Project|Diary, selectedPage?: number): Promise<string> {
|
||||||
if(!content) return projectEmpty;
|
if(!content) return projectEmpty;
|
||||||
switch (content.type) {
|
switch (content.type) {
|
||||||
@ -26,7 +31,7 @@ async function generateProjectHTML(project: Project): Promise<string> {
|
|||||||
<hr>
|
<hr>
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
<div id="footer-text">
|
<div id="footer-text">
|
||||||
Last updated: ${new Date(adDoc.getAttribute("docdatetime")).toLocaleString()} | <a href="https://git.c0ntroller.de/c0ntroller/frontpage-content/src/branch/senpai/projects/${project.name}.adoc" target="_blank">Document source</a>
|
Last updated: ${new Date(adDoc.getAttribute("docdatetime")).toLocaleString()} | <a href="https://git.c0ntroller.de/c0ntroller/frontpage-content/src/branch/${isDev ? "dev" : "senpai"}/projects/${project.name}.adoc" target="_blank">Document source</a>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@ -43,7 +48,7 @@ async function generateDiaryHTML(diary: Diary, selectedPage?: number): Promise<s
|
|||||||
<hr>
|
<hr>
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
<div id="footer-text">
|
<div id="footer-text">
|
||||||
Last updated: ${new Date(adDoc.getAttribute("docdatetime")).toLocaleString()} | <a href="https://git.c0ntroller.de/c0ntroller/frontpage-content/src/branch/senpai/diaries/${gitfile}" target="_blank">Document source</a>
|
Last updated: ${new Date(adDoc.getAttribute("docdatetime")).toLocaleString()} | <a href="https://git.c0ntroller.de/c0ntroller/frontpage-content/src/branch/${isDev ? "dev" : "senpai"}/diaries/${gitfile}" target="_blank">Document source</a>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
@ -7,5 +7,12 @@ module.exports = {
|
|||||||
i18n: {
|
i18n: {
|
||||||
locales: ["en"],
|
locales: ["en"],
|
||||||
defaultLocale: "en",
|
defaultLocale: "en",
|
||||||
}
|
},
|
||||||
|
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.adoc$/i,
|
||||||
|
loader: "raw-loader",
|
||||||
|
});
|
||||||
|
return config;
|
||||||
|
},
|
||||||
};
|
};
|
1471
package-lock.json
generated
1471
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -24,6 +24,7 @@
|
|||||||
"@types/react-dom": "^18.0.5",
|
"@types/react-dom": "^18.0.5",
|
||||||
"eslint": "7.32.0",
|
"eslint": "7.32.0",
|
||||||
"eslint-config-next": "12.0.4",
|
"eslint-config-next": "12.0.4",
|
||||||
|
"raw-loader": "^4.0.2",
|
||||||
"sass": "^1.49.7",
|
"sass": "^1.49.7",
|
||||||
"typescript": "4.5.2"
|
"typescript": "4.5.2"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import "../styles/globals.css";
|
import "../styles/globals.scss";
|
||||||
import { CommandsProvider } from "../lib/commands/ContextProvider";
|
import { CommandsProvider } from "../lib/commands/ContextProvider";
|
||||||
import { ModalFunctionProvider } from "../components/Terminal/contexts/ModalFunctions";
|
import { ModalFunctionProvider } from "../components/Terminal/contexts/ModalFunctions";
|
||||||
|
|
||||||
|
54
pages/blog/project/[pid].tsx
Normal file
54
pages/blog/project/[pid].tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import type { GetServerSideProps, NextPage } from "next";
|
||||||
|
import Head from "next/head";
|
||||||
|
import Navigation from "../../../components/Blog/Navigation";
|
||||||
|
import { generateContent } from "../../../lib/content/generateBackend";
|
||||||
|
import type { ContentList } from "../../../lib/content/types";
|
||||||
|
|
||||||
|
import contentList from "../../../public/content/list.json";
|
||||||
|
|
||||||
|
import styles from "../../../styles/Blog/Content.module.scss";
|
||||||
|
|
||||||
|
interface IContentRender {
|
||||||
|
more?: string;
|
||||||
|
repo?: string;
|
||||||
|
title: string;
|
||||||
|
html: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Post: NextPage<{ content: IContentRender }> = ({ content }) => {
|
||||||
|
return <>
|
||||||
|
<Head>
|
||||||
|
<title>{content.title} - c0ntroller.de</title>
|
||||||
|
</Head>
|
||||||
|
<div id={"blogBody"}>
|
||||||
|
<header>
|
||||||
|
<Navigation />
|
||||||
|
</header>
|
||||||
|
<main dangerouslySetInnerHTML={{ __html: content.html}}>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
|
const { pid } = context.query;
|
||||||
|
const contentEntry = (contentList as ContentList).find((c) => c.name === pid && c.type === "project");
|
||||||
|
|
||||||
|
|
||||||
|
if (!contentEntry) return { notFound: true };
|
||||||
|
|
||||||
|
const contentHtml = await generateContent(contentEntry);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
content: {
|
||||||
|
more: contentEntry.more || null,
|
||||||
|
repo: contentEntry.repo || null,
|
||||||
|
title: contentEntry.title,
|
||||||
|
html: contentHtml
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Post;
|
@ -3,18 +3,21 @@ import Head from "next/head";
|
|||||||
import type { ContentList } from "../lib/content/types";
|
import type { ContentList } from "../lib/content/types";
|
||||||
import Navigation from "../components/Blog/Navigation";
|
import Navigation from "../components/Blog/Navigation";
|
||||||
import ProjectCard from "../components/Blog/Card";
|
import ProjectCard from "../components/Blog/Card";
|
||||||
|
|
||||||
|
import contentList from "../public/content/list.json";
|
||||||
|
|
||||||
import styles from "../styles/Blog/Front.module.scss";
|
import styles from "../styles/Blog/Front.module.scss";
|
||||||
|
|
||||||
const Blog: NextPage<{ content: ContentList }> = ({content}) => {
|
const Blog: NextPage<{ content: ContentList }> = ({content}) => {
|
||||||
const generateCards = (type: string) => {
|
const generateCards = (type: string) => {
|
||||||
return <div className={styles.contentList}>{content.filter(p => p.type === type).map(p => <ProjectCard key={p.name} title={p.title} description={p.desc.join(" ")} />)}</div>;
|
return <div className={styles.contentList}>{content.filter(p => p.type === type).map(p => <ProjectCard key={p.name} title={p.title} description={p.desc.join(" ")} type={p.type} name={p.name} />)}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Head>
|
<Head>
|
||||||
<title>c0ntroller.de</title>
|
<title>c0ntroller.de</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className={styles.container}>
|
<div id={"blogBody"}>
|
||||||
<header>
|
<header>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
</header>
|
</header>
|
||||||
@ -32,11 +35,7 @@ const Blog: NextPage<{ content: ContentList }> = ({content}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps() {
|
export async function getServerSideProps() {
|
||||||
const url = process.env.BASE_URL || "https://c0ntroller.de";
|
return { props: { content: contentList } };
|
||||||
const res = await fetch(`${url}/content/list.json`);
|
|
||||||
const content = await res.json();
|
|
||||||
|
|
||||||
return { props: { content } };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Blog;
|
export default Blog;
|
@ -18,6 +18,6 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
/* .description {
|
||||||
|
|
||||||
}
|
} */
|
0
styles/Blog/Content.module.scss
Normal file
0
styles/Blog/Content.module.scss
Normal file
@ -1,16 +1,3 @@
|
|||||||
.container {
|
|
||||||
|
|
||||||
header {
|
|
||||||
position: sticky;
|
|
||||||
top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentList {
|
.contentList {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -17,3 +17,15 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#blogBody {
|
||||||
|
header {
|
||||||
|
position: sticky;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user