diff --git a/components/ProjectModal.tsx b/components/ProjectModal.tsx index faa6bc7..3a0d141 100644 --- a/components/ProjectModal.tsx +++ b/components/ProjectModal.tsx @@ -1,69 +1,48 @@ import type { NextPage } from "next"; -import { useEffect, useRef, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import asciidoctor from "asciidoctor"; import styles from "../styles/ProjectModal.module.css"; -import type { Project, Diary } from "../lib/projects/types"; +import type { Project, Diary } from "../lib/content/types"; +import { useCommands } from "./contexts/CommandInterface"; +import { generateContent, projectEmpty } from "../lib/content/generate"; +import { useModalFunctions } from "./contexts/ModalFunctions"; //import Link from "next/link"; -interface ModalInput { - project: Project|Diary|undefined; - projectType: "project"|"diary"; - visible: boolean; - setVisible: CallableFunction; -} +const ProjectModal: NextPage = () => { + const [visible, setVisible] = useState(false); + const [diaryPages, setDiaryPages] = useState([]); + const [content, setContent] = useState(projectEmpty); -const ad = asciidoctor(); + const setModalContent = async (content: Project|Diary, selectedPage?: number) => { + if (content.type === "diary") setDiaryPages(content.entries.map(entry => entry.title)); + setContent(await generateContent(content, selectedPage)); + }; + + const { updateCallbacks: updateCmdCallbacks } = useCommands(); + const { updateCallbacks: updateModalCallbacks } = useModalFunctions(); + updateCmdCallbacks({ setModalVisible: setVisible, setModalContent}); + updateModalCallbacks({ setVisible, setContent: setModalContent, setHtml: setContent }); -const ProjectModal: NextPage = ({ project, projectType, visible, setVisible }) => { - const projectEmpty = "
Kein Projekt ausgewählt.
"; - const [projectData, setProjectData] = useState(projectEmpty); const containerRef = useRef(null); - const projectNotFoundHtml = `
Sorry! There is no data for this project. Please check back later to see if that changed!
`; - const projectServerErrorHtml = `
Sorry! A server error happend when the project data was fetched!
`; - - const generateFooter = (project: string, lastUpdate: string) => `
- - `; - useEffect(() => { - if (project && project.name) { - // TODO - // set Spinner - setProjectData("Loading..."); - fetch(`/api/${projectType === "diary" ? "diaries" : "projects"}/${project.name}`).then((res) => { - if (res.status === 404) setProjectData(projectNotFoundHtml); - if (res.status !== 200) setProjectData(projectServerErrorHtml); - res.text().then(data => { - try { - const adDoc = ad.load(data, { attributes: { showtitle: true } }); - setProjectData(adDoc.convert(adDoc).toString() + generateFooter(project.name, adDoc.getAttribute("docdatetime"))); - } catch { - setProjectData(projectServerErrorHtml); - } - }); - }); - } else if (typeof project === "undefined") setProjectData(projectEmpty); - }, [project, projectType, projectEmpty, projectNotFoundHtml, projectServerErrorHtml]); - - useEffect(() => { - if (projectData && containerRef.current && projectData !== "") { - containerRef.current.innerHTML = projectData; + if (content && containerRef.current) { + containerRef.current.innerHTML = content; } - - }, [projectData, visible]); + }, [content]); if (!visible) return <>; return
- setVisible(false)}> + setVisible(false)}>
X
+ { + // TODO + // If diaryPages + // Show page selector + }
diff --git a/components/REPL/REPLHistory.tsx b/components/REPL/REPLHistory.tsx index 7e56690..b41b544 100644 --- a/components/REPL/REPLHistory.tsx +++ b/components/REPL/REPLHistory.tsx @@ -1,6 +1,6 @@ import { NextPage } from "next"; import Link from "next/link"; -import { BaseSyntheticEvent, MutableRefObject, useEffect, useRef } from "react"; +import type { BaseSyntheticEvent, MutableRefObject } from "react"; import styles from "../../styles/REPL/REPLHistory.module.css"; interface REPLHistoryParams { diff --git a/components/REPL/REPLInput.tsx b/components/REPL/REPLInput.tsx index 50cf107..31cb10b 100644 --- a/components/REPL/REPLInput.tsx +++ b/components/REPL/REPLInput.tsx @@ -1,27 +1,17 @@ import type { NextPage } from "next"; -import useSWR from "swr"; -import { MutableRefObject, useState, createRef, useEffect } from "react"; +import { MutableRefObject, useState, createRef } from "react"; import { CommandInterface } from "../../lib/commands"; import styles from "../../styles/REPL/REPLInput.module.css"; -import type { ProjectList } from "../../lib/projects/types"; +import { useCommands } from "../contexts/CommandInterface"; +import { useModalFunctions } from "../contexts/ModalFunctions"; interface REPLInputParams { historyCallback: CallableFunction; historyClear: CallableFunction; inputRef: MutableRefObject; - modalManipulation: { - setModalVisible: CallableFunction; - setModalProject: CallableFunction; - setModalProjectType: CallableFunction; - } } -async function fetchProjects(endpoint: string): Promise { - const res = await fetch(endpoint); - return res.json(); -} - -const REPLInput: NextPage = ({historyCallback, historyClear, inputRef, modalManipulation}) => { +const REPLInput: NextPage = ({historyCallback, historyClear, inputRef}) => { const typed = createRef(); const completion = createRef(); const [currentCmd, setCurrentCmd] = useState([]); @@ -29,8 +19,8 @@ const REPLInput: NextPage = ({historyCallback, historyClear, in const [inCmdHistory, setInCmdHistory] = useState(-1); const [cmdHistory, setCmdHistory] = useState([]); const [usrInputTmp, setUsrInputTmp] = useState(""); - const [cmdIf, setCmdIf] = useState(new CommandInterface(modalManipulation, {projects: [], diaries: []})); - const { data: projects, error: projectsError } = useSWR("/api/projects?swr=1", fetchProjects); + const {cmdContext: cmdIf} = useCommands(); + const { modalFunctions } = useModalFunctions(); const setInput = (inputRef: HTMLInputElement, input: string) => { const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set; @@ -38,8 +28,8 @@ const REPLInput: NextPage = ({historyCallback, historyClear, in nativeSetter.call(inputRef, input); //if(typed.current) typed.current.innerHTML = input; //if(completion.current) completion.current.innerHTML = ""; - const event = new Event("input", { bubbles: true }); - inputRef.dispatchEvent(event); + inputRef.dispatchEvent(new Event("input", { bubbles: true })); + inputRef.dispatchEvent(new Event("change", { bubbles: true })); }; const clearInput = (inputRef: HTMLInputElement) => { @@ -83,74 +73,68 @@ const REPLInput: NextPage = ({historyCallback, historyClear, in return false; } else setJustTabbed(0); - if (e.key === "Enter") { - e.preventDefault(); - const command = (e.target as HTMLInputElement).value.trim(); - if (cmdHistory.at(-1) !== command) setCmdHistory(cmdHistory.concat([command]).splice(0, 100)); - clearInput(input); - setInCmdHistory(-1); - setCurrentCmd([]); - if (command === "clear") { + switch (true) { + case e.key === "Enter": { + e.preventDefault(); + const command = (e.target as HTMLInputElement).value.trim(); + if (cmdHistory.at(-1) !== command) setCmdHistory(cmdHistory.concat([command]).splice(0, 100)); + clearInput(input); + setInCmdHistory(-1); + setCurrentCmd([]); + if (command === "clear") { + historyClear(); + return false; + } + const result = cmdIf.executeCommand(command); + historyCallback(result); + return false; + } + case e.key === "d" && e.ctrlKey: { + e.preventDefault(); + const result = cmdIf.executeCommand("exit"); + clearInput(input); + historyCallback(result); + return false; + } + case e.key === "l" && e.ctrlKey: { + e.preventDefault(); + clearInput(input); historyClear(); return false; } - const result = cmdIf.executeCommand(command); - historyCallback(result); - return false; - } - - if (e.key === "d" && e.ctrlKey) { - e.preventDefault(); - const result = cmdIf.executeCommand("exit"); - clearInput(input); - historyCallback(result); - return false; - } - - if (e.key === "l" && e.ctrlKey) { - e.preventDefault(); - clearInput(input); - historyClear(); - return false; - } - - if ((e.key === "c" || e.key === "u") && e.ctrlKey) { - e.preventDefault(); - clearInput(input); - return false; - } - - if (e.key === "ArrowUp") { - e.preventDefault(); - const idx = inCmdHistory + 1; - if (idx < cmdHistory.length) { - if (inCmdHistory === -1) setUsrInputTmp(input.value); - - const cmd = cmdHistory[cmdHistory.length - idx - 1]; - setInput(input, cmd); - setInCmdHistory(idx); + case (e.key === "c" || e.key === "u") && e.ctrlKey: { + e.preventDefault(); + clearInput(input); + return false; } - } + case e.key === "ArrowUp": { + e.preventDefault(); + const idx = inCmdHistory + 1; + if (idx < cmdHistory.length) { + if (inCmdHistory === -1) setUsrInputTmp(input.value); - if (e.key === "ArrowDown") { - e.preventDefault(); - const idx = inCmdHistory - 1; - if (0 <= idx) { - const cmd = cmdHistory[cmdHistory.length - idx - 1]; - setInput(input, cmd); - setInCmdHistory(idx); - } else if (idx === -1) { - setInput(input, usrInputTmp); - setInCmdHistory(-1); + const cmd = cmdHistory[cmdHistory.length - idx - 1]; + setInput(input, cmd); + setInCmdHistory(idx); + } + return false; + } + case e.key === "ArrowDown": { + e.preventDefault(); + const idx = inCmdHistory - 1; + if (0 <= idx) { + const cmd = cmdHistory[cmdHistory.length - idx - 1]; + setInput(input, cmd); + setInCmdHistory(idx); + } else if (idx === -1) { + setInput(input, usrInputTmp); + setInCmdHistory(-1); + } + return false; } } }; - useEffect(() => { - if (!projectsError && projects) setCmdIf(new CommandInterface(modalManipulation, projects)); - // In any other case we just don't create another interface. - }, [projects, projectsError, modalManipulation]); - return
diff --git a/components/REPL/index.tsx b/components/REPL/index.tsx index b685480..6b12809 100644 --- a/components/REPL/index.tsx +++ b/components/REPL/index.tsx @@ -6,14 +6,9 @@ import type { NextPage } from "next"; interface IREPLProps { inputRef: MutableRefObject; - modalManipulation: { - setModalVisible: CallableFunction; - setModalProject: CallableFunction; - setModalProjectType: CallableFunction; - } } -const REPL: NextPage = ({ inputRef, modalManipulation }) => { +const REPL: NextPage = ({ inputRef }) => { const date = new Date(); const [history, manipulateHistory] = useState([`cer0 0S - ${date.toLocaleDateString()}`]); const containerRef = useRef(null); @@ -30,7 +25,7 @@ const REPL: NextPage = ({ inputRef, modalManipulation }) => { return (
- +
); }; diff --git a/components/contexts/CommandInterface.tsx b/components/contexts/CommandInterface.tsx new file mode 100644 index 0000000..bf0586e --- /dev/null +++ b/components/contexts/CommandInterface.tsx @@ -0,0 +1,18 @@ +import { createContext, useContext } from "react"; +import type { PropsWithChildren } from "react"; +import { CommandInterface } from "../../lib/commands"; +import type { Diary, Project, ContentList } from "../../lib/content/types"; + +interface CommandInterfaceCallbacks { + setModalVisible?: (visible: boolean) => void; + setModalContent?: (content: Project | Diary, selectedPage?: number) => void; +} + +const commandInterface = new CommandInterface(); +const CommandContext = createContext(commandInterface); +const setCommandCallbacks = (callbacks: CommandInterfaceCallbacks) => commandInterface.callbacks = {...commandInterface.callbacks, ...callbacks}; +const setContents = (content: ContentList) => commandInterface.content = content; +const useCommands = () => ({cmdContext: useContext(CommandContext), updateCallbacks: setCommandCallbacks, setContents}); +const CommandsProvider = (props: PropsWithChildren<{}>) => ; + +export { CommandsProvider, useCommands }; \ No newline at end of file diff --git a/components/contexts/ModalFunctions.tsx b/components/contexts/ModalFunctions.tsx new file mode 100644 index 0000000..400b70b --- /dev/null +++ b/components/contexts/ModalFunctions.tsx @@ -0,0 +1,17 @@ +import { createContext, useContext } from "react"; +import type { PropsWithChildren } from "react"; +import type { Project, Diary } from "../../lib/content/types"; + +interface ModalFunctions { + setVisible?: CallableFunction; + setContent?: (content: Project| Diary) => void; + setHtml?: (html: string) => void; +} + +const modalFunctions: ModalFunctions = {}; +const ModalContext = createContext(modalFunctions); +const updateCallbacks = (callbacks: ModalFunctions) => Object.assign(modalFunctions, callbacks); +const useModalFunctions = () => ({modalFunctions: useContext(ModalContext), updateCallbacks}); +const ModalFunctionProvider = (props: PropsWithChildren<{}>) => ; + +export { ModalFunctionProvider, useModalFunctions }; \ No newline at end of file diff --git a/lib/commands/definitions.ts b/lib/commands/definitions.ts index 4f037ad..6233848 100644 --- a/lib/commands/definitions.ts +++ b/lib/commands/definitions.ts @@ -1,3 +1,4 @@ +import type { Diary, Project } from "../content/types"; import type { Command, Flag } from "./types"; function getCommandByName(name: string): Command|undefined { @@ -146,9 +147,16 @@ const project: Command = { execute: (flags, args, _raw, cmdIf) => { if (project.flags && checkFlagInclude(flags, project.flags.list)) { const result = ["Found the following projects:"]; - cmdIf.projects.projects.forEach(project => result.push(`\t${project.name}\t${project.short_desc}`)); + + const projects = cmdIf.content.filter(p => p.type === "project"); + if(projects.length === 0) result.push("\tNo projects found."); + else projects.forEach(project => result.push(`\t${project.name}\t${project.short_desc}`)); + result.push("And the following diaries:"); - cmdIf.projects.diaries.forEach(diary => result.push(`\t${diary.name}\t${diary.short_desc}`)); + const diaries = cmdIf.content.filter(p => p.type === "diary"); + if(diaries.length === 0) result.push("\tNo diaries found."); + else diaries.forEach(diary => result.push(`\t${diary.name}\t${diary.short_desc}`)); + return result; } @@ -156,8 +164,7 @@ const project: Command = { if (args[0] === "this") args[0] = "homepage"; - let [pjt, ptype] = [cmdIf.projects.projects.find(p => p.name === args[0]), "project"]; - if (!pjt) [pjt, ptype] = [cmdIf.projects.diaries.find(p => p.name === args[0]), "diary"]; + let [pjt] = [cmdIf.content.find(p => p.name === args[0]) as Project|Diary|undefined]; if (!pjt) return [ `Cannot find project ${args[0]}!`, "You can see available projects using 'project -l'." @@ -173,9 +180,8 @@ const project: Command = { } if (project.flags && checkFlagInclude(flags, project.flags.minimal)) return pjt.desc; - cmdIf.callbacks.setModalProjectType(ptype); - cmdIf.callbacks.setModalProject(pjt); - cmdIf.callbacks.setModalVisible(true); + if (cmdIf.callbacks?.setModalContent) cmdIf.callbacks.setModalContent(pjt); + if (cmdIf.callbacks?.setModalVisible) cmdIf.callbacks.setModalVisible(true); return []; } }; @@ -203,4 +209,4 @@ const clear: Command = { execute: () => [] }; -export const commandList = [about, help, man, project, exitCmd, clear]; \ No newline at end of file +export const commandList = [about, help, man, project, exitCmd, clear].sort((a, b) => a.name.localeCompare(b.name)); \ No newline at end of file diff --git a/lib/commands/index.ts b/lib/commands/index.ts index 5c6f312..32cf3a6 100644 --- a/lib/commands/index.ts +++ b/lib/commands/index.ts @@ -1,19 +1,18 @@ -import type { ProjectList } from "../projects/types"; +import type { ContentList } from "../content/types"; import { printSyntax, commandList } from "./definitions"; interface CommandInterfaceCallbacks { - setModalVisible: CallableFunction; - setModalProject: CallableFunction; - setModalProjectType: CallableFunction; + setModalVisible?: CallableFunction; + setModalContent?: CallableFunction; } export class CommandInterface { - callbacks: CommandInterfaceCallbacks; - projects: ProjectList; + callbacks?: CommandInterfaceCallbacks; + content: ContentList = []; - constructor(callbacks: CommandInterfaceCallbacks, projects: ProjectList) { + constructor(callbacks?: CommandInterfaceCallbacks, content?: ContentList) { this.callbacks = callbacks; - this.projects = projects; + this.content = content || []; } static commandCompletion(input: string): string[] { diff --git a/lib/content/generate.ts b/lib/content/generate.ts new file mode 100644 index 0000000..31e4c37 --- /dev/null +++ b/lib/content/generate.ts @@ -0,0 +1,44 @@ +import type { Project, Diary } from "./types"; +import asciidoctor from "asciidoctor"; + +export const projectEmpty = "
Kein Projekt ausgewählt.
"; +const projectNotFoundHtml = `
Sorry! There is no data for this project. Please check back later to see if that changed!
`; +const projectServerErrorHtml = `
Sorry! A server error happend when the project data was fetched!
`; + +const ad = asciidoctor(); + +export async function generateContent(content: Project|Diary, selectedPage?: number): Promise { + 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 { + const resp = await fetch(`/content/projects/${project.name}.adoc`); + if (resp.status !== 200) return projectServerErrorHtml; + const adDoc = ad.load(await resp.text(), { attributes: { showtitle: true } }); + return `${adDoc.convert(adDoc).toString()} +
+`; +} + +async function generateDiaryHTML(diary: Diary, selectedPage?: number): Promise { + const resp = selectedPage === undefined ? await fetch(`/content/diaries/${diary.name}.adoc`) : await fetch(`/content/diaries/${diary.name}/${diary.entries[selectedPage].filename}.adoc`); + if (resp.status !== 200) return projectServerErrorHtml; + const adDoc = ad.load(await resp.text(), { attributes: { showtitle: true } }); + const gitfile = selectedPage === undefined ? `${diary.name}.adoc` : `${diary.name}/${diary.entries[selectedPage].filename}.adoc`; + return `${adDoc.convert(adDoc).toString()} +
+`; +} \ No newline at end of file diff --git a/lib/projects/types.ts b/lib/content/types.ts similarity index 65% rename from lib/projects/types.ts rename to lib/content/types.ts index 47acfa8..82c4d16 100644 --- a/lib/projects/types.ts +++ b/lib/content/types.ts @@ -1,9 +1,9 @@ -export interface ProjectList { - projects: Project[]; - diaries: Diary[]; -} +export type ContentList = (Project|Diary)[]; + +export type ContentType = "project" | "diary"; export interface Project { + type: "project"; name: string; desc: string[]; short_desc: string; @@ -12,6 +12,7 @@ export interface Project { } export interface Diary { + type: "diary"; name: string; desc: string[]; short_desc: string; @@ -19,6 +20,6 @@ export interface Diary { repo?: string; entries: { title: string; - path: string; + filename: string; }[]; } \ No newline at end of file diff --git a/pages/_app.tsx b/pages/_app.tsx index 8db6ab9..00fa758 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -2,11 +2,13 @@ import type { AppProps } from "next/app"; import Head from "next/head"; import "../styles/globals.css"; import "../styles/customAsciidoc.scss"; +import { CommandsProvider } from "../components/contexts/CommandInterface"; +import { ModalFunctionProvider } from "../components/contexts/ModalFunctions"; function MyApp({ Component, pageProps }: AppProps) { return <> - + @@ -22,8 +24,8 @@ function MyApp({ Component, pageProps }: AppProps) { - - + + @@ -32,7 +34,11 @@ function MyApp({ Component, pageProps }: AppProps) { - + + + + + ; } diff --git a/pages/index.tsx b/pages/index.tsx index 17949b8..558dc1f 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,32 +1,47 @@ import type { NextPage } from "next"; import Head from "next/head"; import { GithubLogo, InstagramLogo, DiscordLogo, GameController } from "phosphor-react"; -import { useRef, useState } from "react"; +import { useEffect, useRef,useCallback } from "react"; +import { useCommands } from "../components/contexts/CommandInterface"; +import { useModalFunctions } from "../components/contexts/ModalFunctions"; import ProjectModal from "../components/ProjectModal"; import REPL from "../components/REPL"; -import type { Project, Diary } from "../lib/projects/types"; import styles from "../styles/Home.module.css"; const Home: NextPage = () => { const inputRef = useRef(null); - const [modalVisible, setModalVisible] = useState(false); - const [modalProject, setModalProject] = useState(undefined); - const [modalProjectType, setModalProjectType] = useState<"project"|"diary">("project"); + const { modalFunctions } = useModalFunctions(); + const { setContents } = useCommands(); + + const updateProjects = useCallback(async () => { + try { + const res = await fetch("/content/list.json"); + const projects = await res.json(); + setContents(projects); + } catch {} + }, [setContents]); + + updateProjects(); const focusInput = () => { if (inputRef.current) inputRef.current.focus(); }; const hideModalOnEsc = (e: React.KeyboardEvent) => { if (e.key === "Escape") { e.preventDefault(); - setModalVisible(false); + if(modalFunctions.setVisible) modalFunctions.setVisible(false); } }; + useEffect(() => { + const interval = setInterval(updateProjects, 30 * 1000); + return () => clearInterval(interval); + }, [updateProjects]); + return (
c0ntroller.de - +
  @@ -49,7 +64,7 @@ const Home: NextPage = () => {  
- +
); };