From bc09c9df68ed02734d6c39e7b69d6ba97f40e7b0 Mon Sep 17 00:00:00 2001 From: JCTRoth Date: Sat, 20 Dec 2025 05:44:46 +0100 Subject: [PATCH 01/37] Moved copilot instructions to correct folder --- copilot-instructions.md => .github/copilot-instructions.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename copilot-instructions.md => .github/copilot-instructions.md (100%) diff --git a/copilot-instructions.md b/.github/copilot-instructions.md similarity index 100% rename from copilot-instructions.md rename to .github/copilot-instructions.md From 19d14f506a4f31486b2ccf99d49d5a7ccf7aba55 Mon Sep 17 00:00:00 2001 From: JCTRoth Date: Sat, 20 Dec 2025 06:13:04 +0100 Subject: [PATCH 02/37] Add contact, legal, resume, and skills data files --- data/CONTACT.md | 8 ++++ data/favicon-preview.png | Bin 0 -> 1703 bytes data/legal.json | 3 ++ data/resume.json | 79 +++++++++++++++++++++++++++++++++++++++ data/skills.json | 12 ++++++ 5 files changed, 102 insertions(+) create mode 100644 data/CONTACT.md create mode 100644 data/favicon-preview.png create mode 100644 data/legal.json create mode 100644 data/resume.json create mode 100644 data/skills.json diff --git a/data/CONTACT.md b/data/CONTACT.md new file mode 100644 index 0000000..cb4f631 --- /dev/null +++ b/data/CONTACT.md @@ -0,0 +1,8 @@ +Have questions or want to get in touch? Use the email button below. + +- Job opportunities +- Collaboration ideas +- Open‑source contributions +- Technical questions + +JonasRoth@mailbase.info \ No newline at end of file diff --git a/data/favicon-preview.png b/data/favicon-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..fe260a0acb9085ecf037c93b19d08b555aac9b66 GIT binary patch literal 1703 zcmV;Y23YxtP)#MJc0*pd&?OptWh!q$WPJCQYqYV;4}O*4CO-rB!gLGsB&^bN5+K&-pH^ z%!Sc-laqUy8RmD^|M`aFBD_jFwbx2cY3~+ysJ5b$T3>Oy<|z79^{qdl`U*~{jsmOF zmw#OD%s-}V%4=2UaIfkrwQ11^?nYCJJ2WUhiPo+G!U5q>KxS1S@0bh-hx`^5a*ry? z+Ge>c`-nXMrNNuJ)L9#BT60OK=0`Y00?#wxXfhxivW`f;HHYP<)raJnF9_G>Z?NgN z5e{bXbyfq8X*b}lli+}GKscJzyUHTH5h;9esZDp7!3z<-zD z@lpB#Pm~#aCZ_$C8N3MLIjsshp+MVDUbyy803HqnVZa%LK76+R=w%tmAV&^|g#*GN zt=@AztzMip$${a93!nC3_~tGZZaxfv*AF1+ArMr<=-C?pxc+YdHlc&*2wfc>jwV9t z$3p${@MzO+AFg*1elfvye+cZJ5ICg}49SMkvt0;*?lqjq!3<O7&(H(5b*MIM$X{t&(I0451{3H9ahwOFau%XfN)5v5!%NqoTlWi%uaDd?QF43DX%o=*ZPB4g5l;110P?4-|qSmj2SSC+<-&;Zg($-aOYI;{8kbf z39RRhCVb`z4`)s2Hkz>coCZq~kcQEl!B^~d!}2P@&LP}8-C`ky%RMTw5z`Ss-A>Uu zhtp2a1gH1_AcE^K`dZV-A-+n0WxE8O!!h#~Sf}1<*{JO+QQ=5rNc9LLX-2)t z=$Rt#>kmL-vj_?MriU-zCBV{3x6WbY&Vu7pu2iP&B(J1!Br>fy=RJ1=tlP{HpV{llq(`D!ABr_d+WU;Ka7z&qw(b1e|9}*$QBCh+L zC^VgB@T?K|VRVGw1ExV49OLy(PE_YtN`~`ic%0Zu{Z)2Y$H!Ul0bJhSBp)oRIUke9(PKhpI0X z$U&!K5fZb*4J)YQKbi!W~jUayY6EUNj1hyMxpW08kWbV zfSx@{2o&5E^chjbbAlrPXM4P`*($+;au>wyaA5{U;Sl$st8;W4(&-i9zU(FfOR%m* zhO>P>_T?(BSQo8akFj#u{EyhM4vhS57lNnz{Q<#!|dh#>0{d z2KUBpcg>w}Ccv~e(P`#An*x6f1X&~_P&4J5v6$ZbJvk zU6x5FBCPq$7MAgY3%2qx5&bs6Q_Sn}HNutJZ#3wCX7mF&GQJTBGTz2F zpj$TTe(z}jDnFATdJEsPpp1`tp Date: Mon, 22 Dec 2025 05:24:30 +0100 Subject: [PATCH 03/37] Added ADoc/AsciiDoc Support --- src/components/AllCodeStats.tsx | 4 +- src/components/ProjectCard.tsx | 8 ++-- src/pages/ProjectDetail.tsx | 23 ++++----- src/types/index.ts | 9 +++- src/utils/asciidocLoader.ts | 77 +++++++++++++++++++++++++++++++ src/utils/markdownLoader.ts | 82 ++++++++++++++++++++++----------- src/utils/previews.ts | 4 +- 7 files changed, 161 insertions(+), 46 deletions(-) create mode 100644 src/utils/asciidocLoader.ts diff --git a/src/components/AllCodeStats.tsx b/src/components/AllCodeStats.tsx index dddefe2..ddcbaa3 100644 --- a/src/components/AllCodeStats.tsx +++ b/src/components/AllCodeStats.tsx @@ -15,10 +15,10 @@ function buildFolderToOverrides(): Record> { const res: Record> = {}; const list = (projectsData as any).projects as Array; for (const p of list) { - const md: string | undefined = p.markdownUrl; + const docUrl: string | undefined = p.docUrl || p.markdownUrl; if (!md) continue; // markdownUrl: src/data/projects//README.md - const m = md.match(/src\/data\/projects\/([^/]+)\//i); + const m = docUrl.match(/src\/data\/projects\/([^/]+)\//i); const folder = m?.[1]; if (!folder) continue; if (p['cloc-mapping-overwrite']) { diff --git a/src/components/ProjectCard.tsx b/src/components/ProjectCard.tsx index ab189fb..b1ede0a 100644 --- a/src/components/ProjectCard.tsx +++ b/src/components/ProjectCard.tsx @@ -45,13 +45,13 @@ const ProjectCard: React.FC<{ project: Project }> = ({ project }) => { // Derive folder from markdownUrl to match how ProjectDetail resolves cloc.json // Derive total LOC; if project lacks markdown or cloc file we simply omit the badge (no placeholder shown). const totalLoc = React.useMemo(() => { - const md = project.markdownUrl; - if (!md) return null; - const m = md.match(/src\/data\/projects\/([^/]+)\//i); + const docUrl = project.docUrl || project.markdownUrl; + if (!docUrl) return null; + const m = docUrl.match(/src\/data\/projects\/([^/]+)\//i); const folder = m?.[1]; if (!folder) return null; return locMap[folder] ?? null; - }, [project.markdownUrl, locMap]); + }, [project.docUrl, project.markdownUrl, locMap]); React.useEffect(() => { const v = videoRef.current; diff --git a/src/pages/ProjectDetail.tsx b/src/pages/ProjectDetail.tsx index 0a6463f..d81be1f 100644 --- a/src/pages/ProjectDetail.tsx +++ b/src/pages/ProjectDetail.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useParams, Link } from 'react-router-dom'; import projectsData from '../data/projects.json'; -import loadMarkdown, { getMarkdownAssetUrl } from '../utils/markdownLoader'; +import { loadDocumentation, getMarkdownAssetUrl } from '../utils/markdownLoader'; import loadCloc from '../utils/clocLoader'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; @@ -34,13 +34,14 @@ const ProjectDetail: React.FC = () => { const [md, setMd] = React.useState(''); const [loading, setLoading] = React.useState(false); const [cloc, setCloc] = React.useState(null); - const markdownUrl = project?.markdownUrl; - // derive cloc.json path from markdown or project folder: expect cloc at src/data/projects//cloc.json + const docUrl = project?.docUrl || project?.markdownUrl; + // derive cloc.json path from docUrl or project folder: expect cloc at src/data/projects//cloc.json const clocUrl = React.useMemo(() => { - if (!project?.markdownUrl) return undefined; - // markdownUrl is like 'src/data/projects/CAE/README.md' => replace README.md with cloc.json - return project.markdownUrl.replace(/README\.md$/i, 'cloc.json'); - }, [project]); + if (!docUrl) return undefined; + // Handle both .md and .adoc/.asciidoc files + const cleanedUrl = docUrl.replace(/README\.(md|adoc|asciidoc)$/i, 'cloc.json'); + return cleanedUrl; + }, [docUrl]); // Scroll to top whenever this detail page mounts or the project id changes React.useEffect(() => { @@ -56,9 +57,9 @@ const ProjectDetail: React.FC = () => { React.useEffect(() => { let mounted = true; async function run() { - if (!markdownUrl) return; + if (!docUrl) return; setLoading(true); - const text = await loadMarkdown(markdownUrl); + const text = await loadDocumentation(docUrl); if (mounted) { setMd(text); setLoading(false); @@ -68,7 +69,7 @@ const ProjectDetail: React.FC = () => { return () => { mounted = false; }; - }, [markdownUrl]); + }, [docUrl]); React.useEffect(() => { let mounted = true; @@ -83,7 +84,7 @@ const ProjectDetail: React.FC = () => { }; }, [clocUrl]); - const markdownComponents = useMarkdownComponents(markdownUrl || ''); + const markdownComponents = useMarkdownComponents(docUrl || ''); // Match AllCodeStats behavior: collapsed on small screens, expanded on lg const { width } = useWindowSize(); diff --git a/src/types/index.ts b/src/types/index.ts index d59f30b..139bb9f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -17,11 +17,18 @@ export interface Project { /** Flexible end date, same formats as date_from; can also be the literal "today" */ date_to?: string; /** - * Optional URL to markdown content for detailed project description. + * Optional URL to documentation content for detailed project description. * Can be: * - A relative path to a local markdown file within src (e.g., * "/src/data/projects//README.md" or "./data/projects//README.md") + * - A relative path to a local AsciiDoc file (e.g., + * "/src/data/projects//README.adoc") * - A public URL (e.g., a GitHub README URL) + * Supports both .md (Markdown) and .adoc/.asciidoc (AsciiDoc) files + */ + docUrl?: string; + /** + * @deprecated Use docUrl instead (supports both Markdown and AsciiDoc) */ markdownUrl?: string; } diff --git a/src/utils/asciidocLoader.ts b/src/utils/asciidocLoader.ts new file mode 100644 index 0000000..56c751a --- /dev/null +++ b/src/utils/asciidocLoader.ts @@ -0,0 +1,77 @@ +/** + * AsciiDoc Loader + * + * Lightweight AsciiDoc to HTML converter for CV webpage. + * Supports basic AsciiDoc syntax for project documentation. + */ + +// Basic AsciiDoc to HTML conversion rules +const ADOC_RULES = [ + // Headers + { pattern: /^= (.+)$/gm, replacement: '

$1

' }, + { pattern: /^== (.+)$/gm, replacement: '

$1

' }, + { pattern: /^=== (.+)$/gm, replacement: '

$1

' }, + { pattern: /^==== (.+)$/gm, replacement: '

$1

' }, + + // Bold and italic + { pattern: /\*\*(.+?)\*\*/g, replacement: '$1' }, + { pattern: /\*(.+?)\*/g, replacement: '$1' }, + + // Monospace + { pattern: /`(.+?)`/g, replacement: '$1' }, + + // Links + { pattern: /link:(.+?)\[(.+?)\]/g, replacement: '$2' }, + + // Images + { pattern: /image::(.+?)\[(.+?)\]/g, replacement: '$2' }, + + // Unordered lists + { pattern: /^\* (.+)$/gm, replacement: '
  • $1
  • ' }, + + // Paragraphs (simple approach) + { pattern: /^(.+)$/gm, replacement: (match, p1) => { + if (!match.trim().startsWith('<') && !match.trim().startsWith('*')) { + return `

    ${p1}

    `; + } + return match; + }} +]; + +export function convertAsciiDocToHtml(adocContent: string): string { + if (!adocContent || typeof adocContent !== 'string') { + return ''; + } + + let html = adocContent; + + // Apply all conversion rules + ADOC_RULES.forEach(rule => { + html = html.replace(rule.pattern, rule.replacement); + }); + + // Wrap unordered list items in
      tags + html = html.replace(/
    • (.*?)<\/li>/g, (match) => { + if (!html.includes('
        ')) { + return `
          ${match}
        `; + } + return match; + }); + + // Basic HTML structure + return `
        ${html}
        `; +} + +export function isAsciiDocFile(filename: string): boolean { + return filename.toLowerCase().endsWith('.adoc') || filename.toLowerCase().endsWith('.asciidoc'); +} + +export function getFileExtension(filename: string): string { + return filename.split('.').pop()?.toLowerCase() || ''; +} + +export default { + convertAsciiDocToHtml, + isAsciiDocFile, + getFileExtension +}; \ No newline at end of file diff --git a/src/utils/markdownLoader.ts b/src/utils/markdownLoader.ts index 2ff8422..ada1eab 100644 --- a/src/utils/markdownLoader.ts +++ b/src/utils/markdownLoader.ts @@ -1,10 +1,18 @@ -// Lightweight loader that supports loading markdown either from: +// Lightweight loader that supports loading documentation either from: // - a local path within src/data or src/data/projects (bundled as an asset) // - an external http(s) URL -// Returns the markdown text or an empty string on failure. +// Supports both Markdown (.md) and AsciiDoc (.adoc, .asciidoc) formats +// Returns the HTML content or an empty string on failure. const isExternal = (url: string) => /^https?:\/\//i.test(url); +// Import AsciiDoc utilities +import { + convertAsciiDocToHtml, + isAsciiDocFile, + getFileExtension +} from './asciidocLoader'; + // If a path points to something inside the public folder, normalize to a URL the browser can fetch. function toPublicPath(url: string): string | undefined { // Absolute path starting with '/' is already a public URL in CRA @@ -115,47 +123,69 @@ export function getMarkdownAssetUrl(markdownUrl?: string): string | undefined { return pub; } -export async function loadMarkdown(markdownUrl?: string): Promise { - if (!markdownUrl) return ''; +export async function loadDocumentation(docUrl?: string): Promise { + if (!docUrl) return ''; try { - if (isExternal(markdownUrl)) { - const res = await fetch(markdownUrl); + const fileExtension = getFileExtension(docUrl); + const isAdoc = isAsciiDocFile(docUrl); + + if (isExternal(docUrl)) { + const res = await fetch(docUrl); if (!res.ok) return ''; const ct = res.headers.get('content-type') || ''; - // Only treat as markdown if not HTML + // Only treat as documentation if not HTML if (/text\/html/i.test(ct)) { return ''; } - // Accept text/markdown, text/plain, or unknown types - return await res.text(); + const content = await res.text(); + + // Convert to HTML based on file type + if (isAdoc || fileExtension === 'adoc' || fileExtension === 'asciidoc') { + return convertAsciiDocToHtml(content); + } + // For markdown, return as-is (will be processed by react-markdown) + return content; } - const key = resolveLocalKey(markdownUrl); + const key = resolveLocalKey(docUrl); + let content = ''; + if (viteMdModules && key && viteMdModules[key]) { // Vite eager raw content is already a string - return String(viteMdModules[key]); - } - - const assetUrl = getMarkdownAssetUrl(markdownUrl); - if (assetUrl) { - const res = await fetch(assetUrl); - if (!res.ok) return ''; - return await res.text(); + content = String(viteMdModules[key]); + } else { + const assetUrl = getMarkdownAssetUrl(docUrl); + if (assetUrl) { + const res = await fetch(assetUrl); + if (!res.ok) return ''; + content = await res.text(); + } else { + // Final fallback: attempt fetching a normalized public path + const pub = toPublicPath(docUrl); + if (pub) { + const res = await fetch(pub); + if (!res.ok) return ''; + content = await res.text(); + } + } } - // Final fallback: attempt fetching a normalized public path - const pub = toPublicPath(markdownUrl); - if (pub) { - const res = await fetch(pub); - if (!res.ok) return ''; - return await res.text(); + + // Convert to HTML based on file type + if (isAdoc || fileExtension === 'adoc' || fileExtension === 'asciidoc') { + return convertAsciiDocToHtml(content); } + + // For markdown, return as-is (will be processed by react-markdown) + return content; - return ''; } catch (e) { // eslint-disable-next-line no-console - console.warn('Failed to load markdown:', e); + console.warn('Failed to load documentation:', e); return ''; } } +// Backward compatibility: alias loadMarkdown to loadDocumentation +export const loadMarkdown = loadDocumentation; + export default loadMarkdown; diff --git a/src/utils/previews.ts b/src/utils/previews.ts index d0cd79b..0fb65be 100644 --- a/src/utils/previews.ts +++ b/src/utils/previews.ts @@ -73,7 +73,7 @@ export function getProjectPreviewUrl(project: Project): string | undefined { if (!previewEntries.length) return undefined; // 1) If project has a local markdownUrl, try to use its directory - const mdDir = dirFromMarkdownUrl(project.markdownUrl ?? ''); + const mdDir = dirFromMarkdownUrl(project.docUrl || project.markdownUrl || ''); if (mdDir) { const match = previewEntries.find((e) => e.dir.toLowerCase().endsWith(mdDir.toLowerCase())); if (match) return match.url; @@ -96,7 +96,7 @@ export function getProjectPreviewVideoSources(project: Project): ProjectPreviewV if (!previewVideoEntries.length) return []; // 1) Try to resolve by markdown directory (local paths only) - const mdDir = dirFromMarkdownUrl(project.markdownUrl ?? ''); + const mdDir = dirFromMarkdownUrl(project.docUrl || project.markdownUrl || ''); let matches: VideoEntry[] = []; if (mdDir) { matches = previewVideoEntries.filter((e) => e.dir.toLowerCase().endsWith(mdDir.toLowerCase())); From d2538795f25a6708928c58bef003ffb91e215ebb Mon Sep 17 00:00:00 2001 From: JCTRoth Date: Mon, 22 Dec 2025 06:15:37 +0100 Subject: [PATCH 04/37] Update favicon references, add scripts for generating preview placeholders, and improve code logic --- .gitignore | 3 + index.html | 4 +- public/index.html | 1 + scripts/generate-preview-placeholders.js | 106 +++++++++++++++++++ scripts/simple-preview-placeholders.js | 127 +++++++++++++++++++++++ src/App.tsx | 2 +- src/components/AllCodeStats.tsx | 2 +- 7 files changed, 241 insertions(+), 4 deletions(-) create mode 100644 scripts/generate-preview-placeholders.js create mode 100644 scripts/simple-preview-placeholders.js diff --git a/.gitignore b/.gitignore index 0fdfc73..71f7e33 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,6 @@ temp/ # Ensure lockfiles remain tracked # (do NOT ignore package-lock.json, yarn.lock, pnpm-lock.yaml) + +# Ignore all git projects in a specific folder +git_projects/ \ No newline at end of file diff --git a/index.html b/index.html index e25f4d2..4d61b53 100644 --- a/index.html +++ b/index.html @@ -4,8 +4,8 @@ Personal CV - - + +
        diff --git a/public/index.html b/public/index.html index fa30b41..b4a1d5b 100644 --- a/public/index.html +++ b/public/index.html @@ -3,6 +3,7 @@ + Vite + React + TS