From 1f527b196c073775e8f183401237d2040634007b Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Tue, 10 Dec 2024 22:53:13 +0100 Subject: [PATCH 01/33] chore:move metadata fields for project maturity and featured status in project MDX files --- app/projects/page.tsx | 51 +++++--- components/src/partials/projects.tsx | 8 +- components/src/projectMdxParser.ts | 119 ++++++++++++++++++ components/src/projectUtils.ts | 79 ------------ components/src/utils.ts | 18 +-- content/projects/_template.mdx | 13 +- content/projects/bldrs.mdx | 26 ++-- content/projects/bonsai.mdx | 3 + content/projects/calc.mdx | 14 ++- .../circular-construction-co-pilot.mdx | 3 + content/projects/compas.mdx | 3 + content/projects/ifc-lca.mdx | 15 ++- content/projects/ifc-model-checker.mdx | 3 + content/projects/lcax-and-epdx.mdx | 3 + content/projects/pyrevit.mdx | 3 + content/projects/speckle.mdx | 3 + content/projects/sprint.mdx | 3 + content/projects/that-open-company.mdx | 3 + 18 files changed, 236 insertions(+), 134 deletions(-) create mode 100644 components/src/projectMdxParser.ts delete mode 100644 components/src/projectUtils.ts diff --git a/app/projects/page.tsx b/app/projects/page.tsx index 6622ae8..c489080 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -1,28 +1,32 @@ -import { Button, getPosts, Section } from "@/components"; +import { Button, Section } from "@/components"; import { - Maturity, parseProjects, - Project, validMaturities, -} from "@/components/src/projectUtils"; + MdxProject, + Maturity, + getProjects, +} from "@opensource-construction/components/src/projectMdxParser"; function capitalizeFirstLetter(string: string): string { return string.charAt(0).toUpperCase() + string.slice(1); } export default function Projects() { - let projects = getPosts("projects"); + let projects = getProjects("projects"); let parsedProjects = parseProjects(projects); - const projectsByMaturity = parsedProjects.reduce>( + const projectsByMaturity = parsedProjects.reduce< + Record + >( (acc, project) => { - if (!acc[project.maturity]) { - acc[project.maturity] = []; + const maturity = project.metadata.maturity; // Access maturity through project.project + if (!acc[maturity]) { + acc[maturity] = []; } - acc[project.maturity].push(project); + acc[maturity].push(project); return acc; }, - {} as Record, + {} as Record, ); const sortedProjectsByMaturity = Object.entries(projectsByMaturity).sort( @@ -54,23 +58,30 @@ export default function Projects() {

Projects

- The os.c marketplace is the place to publish open source projects with one - idea in mind: To reduce the incredible duplication of efforts that - really slows down innovation in the AECO industry. + The os.c marketplace is the place to publish open source projects with + one idea in mind: To reduce the incredible duplication of efforts that + really slows down innovation in the AECO industry.

The idea of this space is twofolded:
- 1. it‘s a collaborative repository to collect any code that is open and helpful to others in the AECO sector. - Projects range from small scripts, that you wish you would have at hand instead of building it yourself - to larger projects, that help you move faster. + 1. it‘s a collaborative repository to collect any code that is + open and helpful to others in the AECO sector. Projects range from + small scripts, that you wish you would have at hand instead of + building it yourself to larger projects, that help you move faster.

-

2. get to know the people behind the projects. Learn about their motivation to publish code open source and about the models they have - found to make the work on it sustainable. In addition to the publication on the website, we offer the possibility to set up a community call - that is recorded and also shared on the website. +

+ {" "} + 2. get to know the people behind the projects. Learn about their + motivation to publish code open source and about the models they have + found to make the work on it sustainable. In addition to the + publication on the website, we offer the possibility to set up a + community call that is recorded and also shared on the website.

-

Let‘s keep pushing the industry further. Step by step and never +

+ {" "} + Let‘s keep pushing the industry further. Step by step and never stopping.

diff --git a/components/src/partials/projects.tsx b/components/src/partials/projects.tsx index 8e011f8..5680257 100644 --- a/components/src/partials/projects.tsx +++ b/components/src/partials/projects.tsx @@ -1,8 +1,6 @@ -import { getPosts } from "../utils"; import { Card } from "../card"; -import { parseProjects } from "../projectUtils"; +import { getProjects, parseProjects } from "../projectMdxParser"; import { Button } from "../button"; -import Link from "next/link"; function getRandomItems(array: T[], numItems: number): T[] { const shuffled = array.sort(() => 0.5 - Math.random()); @@ -10,7 +8,7 @@ function getRandomItems(array: T[], numItems: number): T[] { } export function ProjectsPartial() { - let projects = getPosts("projects"); + let projects = getProjects("projects"); const parsedProjects = parseProjects(projects); const numberOfProjects = 3; @@ -18,7 +16,7 @@ export function ProjectsPartial() { let showFeatured = false; const filteredProjects = showFeatured - ? parsedProjects.filter((project) => project.featured) + ? parsedProjects.filter((project) => project.metadata.featured) : getRandomItems(parsedProjects, numberOfProjects); return ( diff --git a/components/src/projectMdxParser.ts b/components/src/projectMdxParser.ts new file mode 100644 index 0000000..b6d66ba --- /dev/null +++ b/components/src/projectMdxParser.ts @@ -0,0 +1,119 @@ +import fs from "fs"; +import path from "path"; +import YAML from "yaml"; +import { parseSlug } from "./utils"; + +// Types & Interfaces +export type Maturity = typeof validMaturities[number]; + +export interface MdxProject { + title: string; + slug: string; + description: string; + metadata: { + featured: boolean; + maturity: Maturity; + }; + links: { + url: string; + label: string; + }[]; + content?: string; +} + +// Constants +export const validMaturities = ["sandbox", "incubation", "graduated"] as const; +const defaultMaturity: Maturity = "sandbox"; + +// File System Utils +function getMDXFiles(dir: string): string[] { + return fs + .readdirSync(dir) + .filter(file => + path.extname(file) === ".mdx" && !path.basename(file).startsWith("_") + ); +} + +function readMDXFile(filePath: string): MdxProject { + const rawContent = fs.readFileSync(filePath, "utf-8"); + const slug = parseSlug(path.basename(filePath, path.extname(filePath))); + return parseFrontmatter(rawContent, slug); +} + +// Parsing Utils +function parseMaturity(maturity: string): Maturity { + const cleanedMaturity = maturity?.trim()?.toLowerCase(); + return validMaturities.includes(cleanedMaturity as Maturity) + ? (cleanedMaturity as Maturity) + : defaultMaturity; +} + +function parseFrontmatter(fileContent: string, slug: string): MdxProject { + try { + const frontmatterRegex = /---\s*([\s\S]*?)\s*---/; + const match = frontmatterRegex.exec(fileContent); + + if (!match) { + throw new Error('No frontmatter found'); + } + + const frontMatterBlock = match[1]; + const content = fileContent.replace(frontmatterRegex, "").trim(); + const parsedMetadata = YAML.parse(frontMatterBlock); + + return validateMetadata({ ...parsedMetadata, content }, slug); + } catch (error) { + console.error('Error parsing frontmatter:', error); + return validateMetadata({ content: fileContent }, slug); + } +} + +// Validation Utils +function validateMetadata(input: any, slug: string = ""): MdxProject { + const defaultProject: MdxProject = { + title: "", + description: "", + slug, + metadata: { + featured: false, + maturity: defaultMaturity + }, + links: [] + }; + + return { + ...defaultProject, + title: input?.title || defaultProject.title, + description: input?.description || defaultProject.description, + slug: input?.slug || slug || defaultProject.slug, + metadata: { + featured: input?.metadata?.featured ?? defaultProject.metadata.featured, + maturity: parseMaturity(input?.metadata?.maturity) + }, + links: Array.isArray(input?.links) ? input.links : defaultProject.links, + content: input?.content + }; +} + +// Public API +export function getProjects(dir?: string): MdxProject[] { + const contentDir = path.join(process.cwd(), "content", dir || ""); + try { + const files = getMDXFiles(contentDir); + return files.map(file => readMDXFile(path.join(contentDir, file))); + } catch (error) { + console.error('Error reading projects:', error); + return []; + } +} + +export function parseProjects(projects: MdxProject[]): MdxProject[] { + return projects.map((project) => { + if (!project) { + console.warn('Invalid project data:', project); + return validateMetadata({}); + } + + return validateMetadata(project); + }); +} \ No newline at end of file diff --git a/components/src/projectUtils.ts b/components/src/projectUtils.ts deleted file mode 100644 index 438ae54..0000000 --- a/components/src/projectUtils.ts +++ /dev/null @@ -1,79 +0,0 @@ -import projectMapJson from "../../content/project-map.json"; - -const defaultMaturity: Maturity = "sandbox"; - -//TODO: Put this file in the correct location -//TODO: Improve import path for projectMapJson - -export interface ProjectMap { - title: string; - slug: string; - featured: boolean; - maturity: Maturity; -} - -//TODO: Maybe rename this to something else than Project -export interface Project { - title: string; - slug: string; - featured: boolean; - maturity: Maturity; - description: string; -} - -export const validMaturities = ["sandbox", "incubation", "graduated"] as const; -export type Maturity = typeof validMaturities[number]; - - -const projectMapBySlug = new Map( - projectMapJson.map((project) => [project.slug, project]) -); - -function parseMaturity(maturity: string): Maturity { - const cleanedMaturity = maturity.trim().toLowerCase(); - return validMaturities.includes(cleanedMaturity as Maturity) - ? (cleanedMaturity as Maturity) - : defaultMaturity; -} - -/** - * Function to parse and merge project data with additional metadata from the json file - * @param projects - Array of raw project data from getPosts function - * @returns Array of parsed and merged project data - * - * Usage example: - * - * ```typescript - * import { getPosts } from "../utils/projectUtils"; - * import { parseProjects } from "./projectUtils"; - * - * const rawProjects = getPosts("projects"); - * const parsedProjects = parseProjects(rawProjects); - * ``` - */ -export function parseProjects(projects: any[]): Project[] { - return projects.map((e) => { - const { title, description, project: projectMetadata } = e.metadata; - const { slug } = e; - - // Base project object from metadata - const baseProject: Project = { - ...projectMetadata, - title, - description, - slug, - maturity: e.maturity as Maturity, - }; - - // Merge extra data from projectMapBySlug - const extraData = projectMapBySlug.get(slug); - if (extraData) { - return { - ...baseProject, - ...extraData, - maturity: parseMaturity(extraData.maturity), - }; - } - return baseProject; - }); -} \ No newline at end of file diff --git a/components/src/utils.ts b/components/src/utils.ts index 4ced7c1..5f4a665 100644 --- a/components/src/utils.ts +++ b/components/src/utils.ts @@ -2,12 +2,22 @@ import fs from "fs"; import path from "path"; import YAML from "yaml"; +//Generic utility function to parse frontmatter from a file + export type Post = { metadata: any; slug: string; content: string; }; +export function parseSlug(filename: string): string { + return filename + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/(^-|-$)/g, ''); +} + + function parseFrontmatter(fileContent: string) { let frontmatterRegex = /---\s*([\s\S]*?)\s*---/; let match = frontmatterRegex.exec(fileContent); @@ -32,14 +42,6 @@ function readMDXFile(filePath: string) { return parseFrontmatter(rawContent); } -export function parseSlug(fileBasename: string) { - let prefix = fileBasename.indexOf("-"); - if (prefix && +fileBasename.slice(0, prefix)) { - return fileBasename.slice(prefix + 1); - } - return fileBasename; -} - function getMDXData(dir: string): Post[] { let mdxFiles = getMDXFiles(dir); return mdxFiles.map((file) => { diff --git a/content/projects/_template.mdx b/content/projects/_template.mdx index 8d28711..ea455a9 100644 --- a/content/projects/_template.mdx +++ b/content/projects/_template.mdx @@ -1,6 +1,9 @@ --- title: Example project description: Example description (Max 1 sentence) +metadata: + featured: false + maturity: sandbox # possible values: sandbox, incubation, graduated links: - url: https://example.com label: Website @@ -9,35 +12,43 @@ links: --- ### Problem + Please describe in a few sentences what problem your solution addresses. ### Solution + Please describe in a few sentences how you approach this problem with your solution. ### Why Open Source? + Please describe in a few sentences why your solution is open source. ### Technology + Please describe in a few bullet points how your tech stack looks like. ### License + What licence(s) do you use? ### Operating Model + How do you maintain and update your solution? Who contributes to your solution (team members of your companies / users and other volunteers / …) Who are your typical customers? How do you earn money? ### About the team + Please describe in a few sentences who are the key people behind the project. ### Contact + For more info, please reach out to: person xyz ### Image / Video Footage + Please insert download link(s) here: Link1 Link2 … - diff --git a/content/projects/bldrs.mdx b/content/projects/bldrs.mdx index 8c69720..6b51da3 100644 --- a/content/projects/bldrs.mdx +++ b/content/projects/bldrs.mdx @@ -1,6 +1,9 @@ --- title: Share by bldrs.ai description: Bldrs specializes in developing web-based CAD collaboration tools designed to meet the needs of modern engineering workflows providing real-time collaboration and visualization capabilities for architects, engineers, and designers. +metadata: + featured: true + maturity: incubation links: - url: http://bldrs.ai label: Website @@ -11,6 +14,7 @@ links: - url: mailto:info@bldrs.ai label: info@bldrs.ai --- + Logo>> Link to slides](https://drive.google.com/file/d/1-48Ry6jGv1O6riFH_OiEhsQOLuOc3Dax/view?usp=sharing) - + diff --git a/content/projects/ifc-model-checker.mdx b/content/projects/ifc-model-checker.mdx index 71beee5..bee424c 100644 --- a/content/projects/ifc-model-checker.mdx +++ b/content/projects/ifc-model-checker.mdx @@ -1,6 +1,9 @@ --- title: IFC Model Checker description: automating basic model checks +metadata: + featured: false + maturity: sandbox links: - url: https://modelcheck.opensource.construction/ label: find the tool here diff --git a/content/projects/lcax-and-epdx.mdx b/content/projects/lcax-and-epdx.mdx index c2e12c3..b13bede 100644 --- a/content/projects/lcax-and-epdx.mdx +++ b/content/projects/lcax-and-epdx.mdx @@ -1,6 +1,9 @@ --- title: LCAx and EPDx description: Facilitates interoperability in sustainability assessments by developing an open data format for LCA results and EPD information, promoting transparent and collaborative environmental impact analysis. +metadata: + featured: false + maturity: sandbox links: - url: https://lcax.kongsgaard.eu label: Details about LCAx diff --git a/content/projects/pyrevit.mdx b/content/projects/pyrevit.mdx index e33cc71..170a2d8 100644 --- a/content/projects/pyrevit.mdx +++ b/content/projects/pyrevit.mdx @@ -1,6 +1,9 @@ --- title: pyRevit description: Rapid Application Development (RAD) Environment for Revit +metadata: + featured: false + maturity: graduated links: - url: https://www.pyrevitlabs.io label: Website diff --git a/content/projects/speckle.mdx b/content/projects/speckle.mdx index fb711b3..cd4b1d5 100644 --- a/content/projects/speckle.mdx +++ b/content/projects/speckle.mdx @@ -1,6 +1,9 @@ --- title: Speckle description: Revolutionizes AEC industry collaboration by providing an open data platform for real-time sharing and project visualization, fostering efficiency and innovation across disciplines. +metadata: + featured: false + maturity: graduated links: - url: https://speckle.systems/ label: Website diff --git a/content/projects/sprint.mdx b/content/projects/sprint.mdx index ffa2b72..11da811 100644 --- a/content/projects/sprint.mdx +++ b/content/projects/sprint.mdx @@ -1,6 +1,9 @@ --- title: sPrint description: Batch print your documents from the Autodesk Construction Cloud in no time! +metadata: + featured: false + maturity: incubation links: - url: https://github.com/PerkinsAndWill-IO/sPrint label: GitHub diff --git a/content/projects/that-open-company.mdx b/content/projects/that-open-company.mdx index c988d97..a0aba02 100644 --- a/content/projects/that-open-company.mdx +++ b/content/projects/that-open-company.mdx @@ -1,6 +1,9 @@ --- title: ThatOpenCompany description: delivering infrastructure for your BIM app in the web +metadata: + featured: false + maturity: graduated links: - url: https://thatopen.com/ label: Website From e3f85b8649c3d67f77e7520ef9d99be27ef93f1e Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:55:20 +0100 Subject: [PATCH 02/33] refactor: migrate utility functions to mdxParser and update imports across components --- app/(single-page)/[pageType]/[slug]/page.tsx | 11 +- app/projects/page.tsx | 20 +-- app/trainings/page.tsx | 29 ++-- components/__tests__/utils.test.ts | 2 +- components/index.ts | 4 +- components/src/mdxParser/contentParser.ts | 146 ++++++++++++++++ components/src/mdxParser/mdxValidators.ts | 165 +++++++++++++++++++ components/src/mdxParser/projectMdxParser.ts | 84 ++++++++++ components/src/partials/events.tsx | 2 +- components/src/partials/faq.tsx | 4 +- components/src/partials/projects.tsx | 9 +- components/src/projectMdxParser.ts | 119 ------------- components/src/types/parserTypes.ts | 138 ++++++++++++++++ components/src/utils.ts | 59 ------- 14 files changed, 572 insertions(+), 220 deletions(-) create mode 100644 components/src/mdxParser/contentParser.ts create mode 100644 components/src/mdxParser/mdxValidators.ts create mode 100644 components/src/mdxParser/projectMdxParser.ts delete mode 100644 components/src/projectMdxParser.ts create mode 100644 components/src/types/parserTypes.ts diff --git a/app/(single-page)/[pageType]/[slug]/page.tsx b/app/(single-page)/[pageType]/[slug]/page.tsx index 24b510c..0a63c27 100644 --- a/app/(single-page)/[pageType]/[slug]/page.tsx +++ b/app/(single-page)/[pageType]/[slug]/page.tsx @@ -1,3 +1,8 @@ +import { + loadPosts, + loadProjects, + loadTrainings, +} from "@opensource-construction/components/src/mdxParser/contentParser"; import { Page, getPosts } from "@opensource-construction/components"; import { notFound } from "next/navigation"; @@ -12,13 +17,13 @@ export async function generateStaticParams() { let posts: SinglePageType[] = []; posts = [ ...posts, - ...getPosts("projects").map((p) => { + ...loadProjects().map((p) => { return { slug: p.slug, pageType: "projects" as PageType }; }), ...getPosts("events").map((p) => { return { slug: p.slug, pageType: "events" as PageType }; }), - ...getPosts("trainings").map((p) => { + ...loadTrainings().map((p) => { return { slug: p.slug, pageType: "trainings" as PageType }; }), ...getPosts("page").map((p) => { @@ -29,7 +34,7 @@ export async function generateStaticParams() { } export default function SinglePage({ params }: { params: SinglePageType }) { - let page = getPosts(params.pageType).find( + let page = loadPosts(params.pageType).find( (page) => page.slug === params.slug, ); diff --git a/app/projects/page.tsx b/app/projects/page.tsx index c489080..3aea643 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -1,23 +1,19 @@ import { Button, Section } from "@/components"; +import { loadProjects } from "@opensource-construction/components/src/mdxParser/contentParser"; import { - parseProjects, - validMaturities, - MdxProject, Maturity, - getProjects, -} from "@opensource-construction/components/src/projectMdxParser"; + Project, + validMaturities, +} from "@/components/src/types/parserTypes"; function capitalizeFirstLetter(string: string): string { return string.charAt(0).toUpperCase() + string.slice(1); } export default function Projects() { - let projects = getProjects("projects"); - let parsedProjects = parseProjects(projects); + let projects = loadProjects(); - const projectsByMaturity = parsedProjects.reduce< - Record - >( + const projectsByMaturity = projects.reduce>( (acc, project) => { const maturity = project.metadata.maturity; // Access maturity through project.project if (!acc[maturity]) { @@ -26,7 +22,7 @@ export default function Projects() { acc[maturity].push(project); return acc; }, - {} as Record, + {} as Record, ); const sortedProjectsByMaturity = Object.entries(projectsByMaturity).sort( @@ -71,7 +67,6 @@ export default function Projects() { building it yourself to larger projects, that help you move faster.

- {" "} 2. get to know the people behind the projects. Learn about their motivation to publish code open source and about the models they have found to make the work on it sustainable. In addition to the @@ -80,7 +75,6 @@ export default function Projects() {

- {" "} Let‘s keep pushing the industry further. Step by step and never stopping.

diff --git a/app/trainings/page.tsx b/app/trainings/page.tsx index da1c809..296d813 100644 --- a/app/trainings/page.tsx +++ b/app/trainings/page.tsx @@ -1,30 +1,27 @@ import { getPosts, Section } from "@/components"; +import { loadTrainings } from "@opensource-construction/components/src/mdxParser/contentParser"; import { TrainingsPartial } from "@/components/src/partials/trainings"; import { TrainingCard } from "@/components/src/trainingCard"; +import { Training } from "@/components/src/types/parserTypes"; export default function Trainings() { - let trainings = getPosts("trainings"); + let trainings = loadTrainings(); return (
- {trainings.map( - ( - { slug, metadata: { title, description, author, image } }, - index, - ) => ( - - ), - )} + {trainings.map((training: Training, index) => ( + + ))}
diff --git a/components/__tests__/utils.test.ts b/components/__tests__/utils.test.ts index 571351b..88a5de8 100644 --- a/components/__tests__/utils.test.ts +++ b/components/__tests__/utils.test.ts @@ -1,5 +1,5 @@ import { test, expect } from "vitest"; -import { parseSlug } from "../src/utils"; +import { parseSlug } from "../src/mdxParser/contentParser"; test("parses basic slug correctly", () => { expect(parseSlug("simple")).toBe("simple"); diff --git a/components/index.ts b/components/index.ts index 9ef1c92..3c1bd4f 100644 --- a/components/index.ts +++ b/components/index.ts @@ -5,7 +5,9 @@ import { Footer } from "./src/footer"; import { CustomMDX } from "./src/mdx"; import { Navbar } from "./src/nav"; import { Form } from "./src/form"; -import { getPosts, parseSlug, Post } from "./src/utils"; +import { Post } from "./src/mdxParser/contentParser"; +import { getPosts } from "./src/mdxParser/contentParser"; +import { parseSlug } from "./src/mdxParser/contentParser"; export type { Post }; export { CustomMDX, Button, Section, Page, Footer, Navbar, Form }; diff --git a/components/src/mdxParser/contentParser.ts b/components/src/mdxParser/contentParser.ts new file mode 100644 index 0000000..42c32a6 --- /dev/null +++ b/components/src/mdxParser/contentParser.ts @@ -0,0 +1,146 @@ +// contentParser.ts +import fs from "fs"; +import path from "path"; +import YAML from "yaml"; +import { Content, Project, Training, contentDefaults, ContentType } from "../types/parserTypes"; +import { validateMetadata } from "./mdxValidators"; + +export function parseSlug(fileBasename: string) { + let prefix = fileBasename.indexOf("-"); + if (prefix && +fileBasename.slice(0, prefix)) { + return fileBasename.slice(prefix + 1); + } + return fileBasename; +} +function parseFrontmatter(content: string) { + const match = /---\s*([\s\S]*?)\s*---/.exec(content); + if (!match) return null; + + const yaml = match[1]; + const body = content.replace(match[0], '').trim(); + + try { + const data = YAML.parse(yaml); + return { data, body }; + } catch (e) { + return null; + } +} + +export function loadContent(dir: string, type: ContentType): T[] { + const contentDir = path.join(process.cwd(), "content", dir); + + try { + return fs.readdirSync(contentDir) + .filter(file => path.extname(file) === ".mdx" && !file.startsWith('_')) + .map(file => { + const content = fs.readFileSync(path.join(contentDir, file), 'utf-8'); + const slug = parseSlug(path.basename(file, '.mdx')); + const parsed = parseFrontmatter(content); + + const defaultContent = contentDefaults[type] as T; + + return parsed + ? validateMetadata(parsed.data, slug, parsed.body, defaultContent, type) + : { ...defaultContent, slug }; + }); + } catch (error) { + console.error(`Error loading ${type}:`, error); + return []; + } +} + +export const loadProjects = () => loadContent('projects', 'project') as Project[]; +export const loadTrainings = () => loadContent('trainings', 'training') as Training[]; +export const loadPosts = (dir: string) => loadContent(dir, 'post') as Post[]; + + +export type ContentValidator = ( + raw: any, + slug: string, + content: string, + defaultContent: T +) => T; + +export function getMDXFiles(dir: string): string[] { + try { + return fs.readdirSync(dir) + .filter(file => + path.extname(file) === ".mdx" && + !path.basename(file).startsWith("_") + ); + } catch (error) { + console.error('Error reading directory:', error); + return []; + } +} + +export function readFile( + filePath: string, + validationFn: Parser, + options = { encoding: 'utf-8' as const } +): T { + try { + const rawContent = fs.readFileSync(filePath, options); + const slug = parseSlug(path.basename(filePath, path.extname(filePath))); + const { metadata, content } = parseMdxFile(rawContent); + return validationFn(content, slug, metadata); + } catch (error) { + console.error('Error reading file:', error); + throw error; + } +} +export function parseMdxFile(fileContent: string): { metadata: Record; content: string; } { + try { + const frontmatterRegex = /---\s*([\s\S]*?)\s*---/; + const match = frontmatterRegex.exec(fileContent); + + if (!match) { + throw new Error('No frontmatter found'); + } + + const frontMatterBlock = match[1]; + const content = fileContent.replace(frontmatterRegex, "").trim(); + const metadata = YAML.parse(frontMatterBlock); + + if (typeof metadata !== 'object' || metadata === null) { + throw new Error('Invalid frontmatter format'); + } + + return { metadata, content }; + } catch (error) { + console.error('Error parsing MDX file:', error); + return { metadata: {}, content: fileContent }; + } +} +export interface Parser { + (content: string, slug: string, metadata: unknown): T; +} + +export interface Post { + metadata: Record; + slug: string; + content: string; + title?: string; + description?: string; +} +export function getPosts(dir?: string): Post[] { + const contentDir = path.join(process.cwd(), "content", dir || ""); + try { + const files = getMDXFiles(contentDir); + return files.map(file => readFile( + path.join(contentDir, file), + (content, slug, metadata) => ({ + metadata: metadata as Record, + slug, + content + }) + ) + ); + } catch (error) { + console.error('Error loading posts:', error); + return []; + } +} + + diff --git a/components/src/mdxParser/mdxValidators.ts b/components/src/mdxParser/mdxValidators.ts new file mode 100644 index 0000000..dd46467 --- /dev/null +++ b/components/src/mdxParser/mdxValidators.ts @@ -0,0 +1,165 @@ +import { Training, Event, Project, ContentType, Maturity, validMaturities, Content } from "../types/parserTypes"; +import { ContentValidator } from "./contentParser"; + +export function validatePostMetadata(metadata: any) { + return metadata || {}; + + /** + * An object containing various metadata validation functions. + * + * @property {Function} project - Validates project metadata. + * @property {Function} training - Validates training metadata. + * @property {Function} post - Validates post metadata. + * @property {Function} event - Validates event metadata. + */ +} export const validators = { + project: validateProjectMetadata, + training: validateTrainingMetadata, + post: validatePostMetadata, + event: validateEventMetadata, +}; + +/** + * Validates and transforms raw training content into a structured format. + * + * @param raw - The raw training content to validate. + * @param slug - The slug identifier for the training content. + * @param content - The main content of the training. + * @param defaultContent - The default content to fall back on if raw content is missing. + * @returns The validated and structured training content. + */ +const validateTraining: ContentValidator = (raw, slug, content, defaultContent) => ({ + ...defaultContent, + title: raw?.title || defaultContent.title, + description: raw?.description || defaultContent.description, + slug, + content, + author: raw?.author || '', + image: raw?.image || '', + links: raw?.links || [], + tags: Array.isArray(raw?.tags) ? raw.tags : [], + metadata: { + ...defaultContent.metadata, + level: raw?.metadata?.level || 'beginner', + duration: raw?.metadata?.duration || '1h', + } +}); + +export function validateTrainingMetadata(metadata: any) { + return metadata || {}; +} + +/** + * Validates and processes raw event data into a structured Event object. + * + * @param raw - The raw event data to validate and process. + * @param slug - The slug identifier for the event. + * @param content - The content of the event. + * @param defaultContent - The default content to use if certain fields are missing in the raw data. + * @returns A validated and structured Event object. + */ +const validateEvent: ContentValidator = (raw, slug, content, defaultContent) => ({ + ...defaultContent, + title: raw?.title || defaultContent.title, + description: raw?.description || defaultContent.description, + slug, + content, + metadata: { + ...defaultContent.metadata, + start: raw?.event?.start || new Date().toISOString(), + duration: raw?.event?.duration, + end: raw?.event?.end, + location: raw?.event?.location, + geo: raw?.event?.geo, + status: raw?.event?.status || 'TENTATIVE', + organizer: raw?.event?.organizer, + url: raw?.event?.url + } +}); + +export function validateEventMetadata(metadata: any) { + return metadata || {}; +} + + +/** + * Validates and constructs a Project object by merging raw input data with default content. + * + * @param raw - The raw project data that needs to be validated and merged. + * @param slug - The unique identifier for the project. + * @param content - The main content of the project. + * @param defaultContent - The default content to fall back on if raw data is incomplete. + * @returns A validated and merged Project object. + */ +const validateProject: ContentValidator = (raw, slug, content, defaultContent) => ({ + ...defaultContent, + title: raw?.title || defaultContent.title, + description: raw?.description || defaultContent.description, + slug, + content, + links: raw?.links || [], + metadata: { + ...defaultContent.metadata, + featured: raw?.metadata?.featured || false, + maturity: raw?.metadata?.maturity || 'sandbox', + } +}); + +export function validateProjectMetadata(metadata: any) { + return { + featured: !!metadata?.featured, + maturity: parseMaturity(metadata?.maturity), + }; +} + +function parseMaturity(maturity: any): Maturity { + const cleanedMaturity = String(maturity || '').trim().toLowerCase(); + return validMaturities.includes(cleanedMaturity as Maturity) + ? (cleanedMaturity as Maturity) + : 'sandbox'; +} + +/** + * A record of content validators for different content types. + * Each validator function takes raw content, a slug, processed content, + * and default content, and returns the validated content. + * + * @type {Record>} + * + * @property {ContentValidator} training - Validator for training content. + * @property {ContentValidator} event - Validator for event content. + * @property {ContentValidator} project - Validator for project content. + * @property {ContentValidator} post - Validator for post content. + * + * The `post` validator function: + * @param {any} raw - The raw content data. + * @param {string} slug - The slug for the content. + * @param {any} content - The processed content. + * @param {any} defaultContent - The default content structure. + * @returns {any} - The validated content with title, description, slug, content, and metadata. + */ +export const contentValidators: Record> = { + training: validateTraining, + event: validateEvent, + project: validateProject, + post: (raw, slug, content, defaultContent) => ({ + ...defaultContent, + title: raw?.title || '', + description: raw?.description || '', + slug, + content, + metadata: raw?.metadata || {} + }) +}; +export function validateMetadata( + raw: any, + slug: string, + content: string, + defaultContent: T, + type: ContentType): T { + const validator = contentValidators[type]; + return validator(raw, slug, content, defaultContent); +} + + + diff --git a/components/src/mdxParser/projectMdxParser.ts b/components/src/mdxParser/projectMdxParser.ts new file mode 100644 index 0000000..3abb6f6 --- /dev/null +++ b/components/src/mdxParser/projectMdxParser.ts @@ -0,0 +1,84 @@ +// import path from "path"; +// import YAML from "yaml"; +// import { getMDXFiles, Post, readFile } from "./utils"; +// import { loadContent } from "./contentParser"; +// import { Project } from "./types/parserTypes"; + +// export type Maturity = typeof validMaturities[number]; + +// export interface MdxProject extends Post { +// title: string; +// description: string; +// metadata: { +// featured: boolean; +// maturity: Maturity; +// }; +// links: { +// url: string; +// label: string; +// }[]; +// } + +// export const validMaturities = ["sandbox", "incubation", "graduated"] as const; +// const defaultMaturity: Maturity = "sandbox"; + +// function parseMaturity(maturity: string): Maturity { +// const cleanedMaturity = maturity?.trim()?.toLowerCase(); +// return validMaturities.includes(cleanedMaturity as Maturity) +// ? (cleanedMaturity as Maturity) +// : defaultMaturity; +// } + +// function projectParser(fileContent: string, slug: string, rawMetadata: any): MdxProject { +// try { +// console.log('Raw metadata before validation:', rawMetadata); +// return validateProject(fileContent, slug, rawMetadata); +// } catch (error) { +// console.error('Error parsing project:', error); +// return validateProject(fileContent, slug, {}); +// } +// } + +// function validateProject(content: string, slug: string, metadata: any): MdxProject { +// const defaultProject: MdxProject = { +// title: "", +// description: "", +// slug, +// metadata: { +// featured: false, +// maturity: defaultMaturity +// }, +// links: [], +// content: "" +// }; + +// // Handle both nested and flat metadata structures +// const metadataFields = metadata.metadata || metadata; +// const featured = metadataFields.featured ?? metadata.featured ?? defaultProject.metadata.featured; +// const maturity = parseMaturity(metadataFields.maturity ?? metadata.maturity); + +// return { +// title: metadata.title || defaultProject.title, +// description: metadata.description || defaultProject.description, +// slug: slug || defaultProject.slug, +// metadata: { +// featured, +// maturity +// }, +// links: Array.isArray(metadata.links) ? metadata.links : defaultProject.links, +// content: content || defaultProject.content +// }; +// } + +// // export function loadProjects(dir?: string): MdxProject[] { +// // const contentDir = path.join(process.cwd(), "content", dir || ""); +// // try { +// // const files = getMDXFiles(contentDir); +// // return files.map(file => readFile(path.join(contentDir, file), projectParser)); +// // } catch (error) { +// // console.error('Error reading projects:', error); +// // return []; +// // } +// // } + +// // export const loadProjects = () => loadContent('projects', 'project') as Project[]; \ No newline at end of file diff --git a/components/src/partials/events.tsx b/components/src/partials/events.tsx index 236813b..fc03c61 100644 --- a/components/src/partials/events.tsx +++ b/components/src/partials/events.tsx @@ -1,4 +1,4 @@ -import { getPosts } from "../utils"; +import { getPosts } from "../mdxParser/contentParser"; import { Card } from "../card"; export function EventsPartial({ showPast = false }: { showPast?: boolean }) { diff --git a/components/src/partials/faq.tsx b/components/src/partials/faq.tsx index 4795db1..742adc3 100644 --- a/components/src/partials/faq.tsx +++ b/components/src/partials/faq.tsx @@ -1,10 +1,10 @@ import { CustomMDX } from "../mdx"; -import { getPosts } from "../utils"; +import { processPosts } from "../utils"; import { Card } from "../card"; export function FAQPartial() { - const faqs = getPosts("faqs"); + const faqs = processPosts("faqs"); return (
diff --git a/components/src/partials/projects.tsx b/components/src/partials/projects.tsx index 5680257..b65c82e 100644 --- a/components/src/partials/projects.tsx +++ b/components/src/partials/projects.tsx @@ -1,6 +1,6 @@ import { Card } from "../card"; -import { getProjects, parseProjects } from "../projectMdxParser"; import { Button } from "../button"; +import { loadProjects } from "../mdxParser/contentParser"; function getRandomItems(array: T[], numItems: number): T[] { const shuffled = array.sort(() => 0.5 - Math.random()); @@ -8,16 +8,15 @@ function getRandomItems(array: T[], numItems: number): T[] { } export function ProjectsPartial() { - let projects = getProjects("projects"); - const parsedProjects = parseProjects(projects); + let projects = loadProjects(); const numberOfProjects = 3; let showFeatured = false; const filteredProjects = showFeatured - ? parsedProjects.filter((project) => project.metadata.featured) - : getRandomItems(parsedProjects, numberOfProjects); + ? projects.filter((project) => project.metadata.featured) + : getRandomItems(projects, numberOfProjects); return (
diff --git a/components/src/projectMdxParser.ts b/components/src/projectMdxParser.ts deleted file mode 100644 index b6d66ba..0000000 --- a/components/src/projectMdxParser.ts +++ /dev/null @@ -1,119 +0,0 @@ -import fs from "fs"; -import path from "path"; -import YAML from "yaml"; -import { parseSlug } from "./utils"; - -// Types & Interfaces -export type Maturity = typeof validMaturities[number]; - -export interface MdxProject { - title: string; - slug: string; - description: string; - metadata: { - featured: boolean; - maturity: Maturity; - }; - links: { - url: string; - label: string; - }[]; - content?: string; -} - -// Constants -export const validMaturities = ["sandbox", "incubation", "graduated"] as const; -const defaultMaturity: Maturity = "sandbox"; - -// File System Utils -function getMDXFiles(dir: string): string[] { - return fs - .readdirSync(dir) - .filter(file => - path.extname(file) === ".mdx" && !path.basename(file).startsWith("_") - ); -} - -function readMDXFile(filePath: string): MdxProject { - const rawContent = fs.readFileSync(filePath, "utf-8"); - const slug = parseSlug(path.basename(filePath, path.extname(filePath))); - return parseFrontmatter(rawContent, slug); -} - -// Parsing Utils -function parseMaturity(maturity: string): Maturity { - const cleanedMaturity = maturity?.trim()?.toLowerCase(); - return validMaturities.includes(cleanedMaturity as Maturity) - ? (cleanedMaturity as Maturity) - : defaultMaturity; -} - -function parseFrontmatter(fileContent: string, slug: string): MdxProject { - try { - const frontmatterRegex = /---\s*([\s\S]*?)\s*---/; - const match = frontmatterRegex.exec(fileContent); - - if (!match) { - throw new Error('No frontmatter found'); - } - - const frontMatterBlock = match[1]; - const content = fileContent.replace(frontmatterRegex, "").trim(); - const parsedMetadata = YAML.parse(frontMatterBlock); - - return validateMetadata({ ...parsedMetadata, content }, slug); - } catch (error) { - console.error('Error parsing frontmatter:', error); - return validateMetadata({ content: fileContent }, slug); - } -} - -// Validation Utils -function validateMetadata(input: any, slug: string = ""): MdxProject { - const defaultProject: MdxProject = { - title: "", - description: "", - slug, - metadata: { - featured: false, - maturity: defaultMaturity - }, - links: [] - }; - - return { - ...defaultProject, - title: input?.title || defaultProject.title, - description: input?.description || defaultProject.description, - slug: input?.slug || slug || defaultProject.slug, - metadata: { - featured: input?.metadata?.featured ?? defaultProject.metadata.featured, - maturity: parseMaturity(input?.metadata?.maturity) - }, - links: Array.isArray(input?.links) ? input.links : defaultProject.links, - content: input?.content - }; -} - -// Public API -export function getProjects(dir?: string): MdxProject[] { - const contentDir = path.join(process.cwd(), "content", dir || ""); - try { - const files = getMDXFiles(contentDir); - return files.map(file => readMDXFile(path.join(contentDir, file))); - } catch (error) { - console.error('Error reading projects:', error); - return []; - } -} - -export function parseProjects(projects: MdxProject[]): MdxProject[] { - return projects.map((project) => { - if (!project) { - console.warn('Invalid project data:', project); - return validateMetadata({}); - } - - return validateMetadata(project); - }); -} \ No newline at end of file diff --git a/components/src/types/parserTypes.ts b/components/src/types/parserTypes.ts new file mode 100644 index 0000000..c2071f2 --- /dev/null +++ b/components/src/types/parserTypes.ts @@ -0,0 +1,138 @@ +// types.ts +export const validMaturities = ["sandbox", "incubation", "graduated"] as const; +export type Maturity = typeof validMaturities[number]; +export type ContentType = 'project' | 'training' | 'post' | 'event'; + +interface BaseContent { + title: string; + description: string; + slug: string; + content: string; +} + +export interface Project extends BaseContent { + type: 'project'; + metadata: { + featured: boolean; + maturity: Maturity; + }; + links: Array<{ url: string; label: string }>; +} + +export type TagCategory = 'type' | 'tool' | 'cost' | 'mode'; + +export function isValidTrainingTag(tag: string): tag is TrainingTag { + return /^(type|tool|cost|mode)::.+$/.test(tag); +} + +export type TagValue = string; +export type TrainingTag = `${TagCategory}::${TagValue}`; + +export interface Training extends BaseContent { + type: 'training'; + title: string; + description: string; + author: string; + image: string; + links: { + url: string; + label: string; + }[]; + tags: TrainingTag[]; + metadata: { + level?: string; + duration?: string; + }; +} + +export interface Post extends BaseContent { + type: 'post'; + metadata: Record; +} + +export type Content = Project | Training | Post | Event; + +// contentDefaults in contentParser.ts or a separate file +export const contentDefaults = { + project: { + type: 'project', + title: '', + description: '', + slug: '', + content: '', + metadata: { + featured: false, + maturity: 'sandbox', + }, + links: [], + }, + training: { + type: 'training', + title: '', + description: '', + slug: '', + content: '', + metadata: { + level: 'beginner', + duration: '1h', + }, + }, + post: { + type: 'post', + title: '', + description: '', + slug: '', + content: '', + metadata: {}, + }, + event: { + type: 'event' as const, + title: '', + description: '', + slug: '', + content: '', + metadata: { + start: new Date().toISOString(), + status: 'TENTATIVE' as EventStatus + } + } +}; + + +// parserTypes.ts + +interface GeoLocation { + lat: number; + lon: number; +} + +interface Organizer { + name: string; + email: string; +} + +interface Duration { + seconds?: number; + minutes?: number; + hours?: number; + days?: number; + weeks?: number; +} + +type EventStatus = 'TENTATIVE' | 'CONFIRMED' | 'CANCELLED'; + +interface EventMetadata { + start: string; // ISO date string + duration?: Duration; + end?: string; // ISO date string + location?: string; + geo?: GeoLocation; + status?: EventStatus; + organizer?: Organizer; + url?: string; +} + +export interface Event extends BaseContent { + type: 'event'; + metadata: EventMetadata; +} diff --git a/components/src/utils.ts b/components/src/utils.ts index 5f4a665..139597f 100644 --- a/components/src/utils.ts +++ b/components/src/utils.ts @@ -1,61 +1,2 @@ -import fs from "fs"; -import path from "path"; -import YAML from "yaml"; -//Generic utility function to parse frontmatter from a file -export type Post = { - metadata: any; - slug: string; - content: string; -}; - -export function parseSlug(filename: string): string { - return filename - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/(^-|-$)/g, ''); -} - - -function parseFrontmatter(fileContent: string) { - let frontmatterRegex = /---\s*([\s\S]*?)\s*---/; - let match = frontmatterRegex.exec(fileContent); - let frontMatterBlock = match![1]; - let content = fileContent.replace(frontmatterRegex, "").trim(); - let metadata = YAML.parse(frontMatterBlock); - - return { metadata, content }; -} - -function getMDXFiles(dir: string) { - return fs - .readdirSync(dir) - .filter( - (file) => - path.extname(file) === ".mdx" && !path.basename(file).startsWith("_"), - ); -} - -function readMDXFile(filePath: string) { - let rawContent = fs.readFileSync(filePath, "utf-8"); - return parseFrontmatter(rawContent); -} - -function getMDXData(dir: string): Post[] { - let mdxFiles = getMDXFiles(dir); - return mdxFiles.map((file) => { - let { metadata, content } = readMDXFile(path.join(dir, file)); - let slug = parseSlug(path.basename(file, path.extname(file))); - - return { - metadata, - slug, - content, - }; - }); -} - -export function getPosts(dir?: string) { - return getMDXData(path.join(process.cwd(), "content", dir || "")); -} From 27f67f6c75935d416e285c45843c4df04d7a11ff Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:51:44 +0100 Subject: [PATCH 03/33] refactor: update imports and remove unused utility functions + document --- app/layout.tsx | 3 +- app/projects/page.tsx | 2 +- app/trainings/page.tsx | 2 +- components/__tests__/mdxValidators.test.ts | 110 +++++++++++ components/__tests__/utils.test.ts | 2 +- components/index.ts | 6 +- components/src/mdxParser/mdxParserTypes.ts | 171 ++++++++++++++++++ .../{contentParser.ts => mdxParsers.ts} | 111 ++++++------ components/src/mdxParser/mdxValidators.ts | 101 ++++++++--- components/src/mdxParser/projectMdxParser.ts | 84 --------- components/src/partials/events.tsx | 2 +- components/src/partials/projects.tsx | 2 +- components/src/types/parserTypes.ts | 138 -------------- components/src/utils.ts | 2 - .../20250202-aec-hackathon-zurich-2025.mdx | 43 +++-- content/projects/_xeokit.mdx | 10 + content/trainings/python_zurich.mdx | 2 +- 17 files changed, 455 insertions(+), 336 deletions(-) create mode 100644 components/__tests__/mdxValidators.test.ts create mode 100644 components/src/mdxParser/mdxParserTypes.ts rename components/src/mdxParser/{contentParser.ts => mdxParsers.ts} (53%) delete mode 100644 components/src/mdxParser/projectMdxParser.ts delete mode 100644 components/src/types/parserTypes.ts diff --git a/app/layout.tsx b/app/layout.tsx index 1ec93a2..6a994ea 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -54,8 +54,7 @@ export default function RootLayout({ title="opensource.construction" logo={logoSvg} menuItems={navItems} - > - + >
{children}
diff --git a/app/projects/page.tsx b/app/projects/page.tsx index 3aea643..871665f 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -4,7 +4,7 @@ import { Maturity, Project, validMaturities, -} from "@/components/src/types/parserTypes"; +} from "@opensource-construction/components/src/mdxParser/parserTypes"; function capitalizeFirstLetter(string: string): string { return string.charAt(0).toUpperCase() + string.slice(1); diff --git a/app/trainings/page.tsx b/app/trainings/page.tsx index 296d813..97bedef 100644 --- a/app/trainings/page.tsx +++ b/app/trainings/page.tsx @@ -2,7 +2,7 @@ import { getPosts, Section } from "@/components"; import { loadTrainings } from "@opensource-construction/components/src/mdxParser/contentParser"; import { TrainingsPartial } from "@/components/src/partials/trainings"; import { TrainingCard } from "@/components/src/trainingCard"; -import { Training } from "@/components/src/types/parserTypes"; +import { Training } from "@opensource-construction/components/src/mdxParser/parserTypes"; export default function Trainings() { let trainings = loadTrainings(); diff --git a/components/__tests__/mdxValidators.test.ts b/components/__tests__/mdxValidators.test.ts new file mode 100644 index 0000000..134e623 --- /dev/null +++ b/components/__tests__/mdxValidators.test.ts @@ -0,0 +1,110 @@ +import { test, expect } from "vitest"; +import { Training } from "../src/mdxParser/mdxParserTypes"; +import { validateTraining } from "../src/mdxParser/mdxValidators"; + +const defaultTraining: Training = { + type: "training", + title: "Default Title", + description: "Default Description", + slug: "default-slug", + content: "Default Content", + author: "", + image: "", + links: [], + tags: [], + metadata: { + level: "beginner", + duration: "1h", + }, +}; + +test("validateTraining with complete raw data", () => { + const raw = { + title: "Custom Title", + description: "Custom Description", + author: "Author Name", + image: "image-url", + links: ["link1", "link2"], + tags: ["tag1", "tag2"], + metadata: { + level: "advanced", + duration: "2h", + }, + }; + const result = validateTraining( + raw, + "custom-slug", + "Custom Content", + defaultTraining, + ); + expect(result).toEqual({ + type: "training", + title: "Custom Title", + description: "Custom Description", + slug: "custom-slug", + content: "Custom Content", + author: "Author Name", + image: "image-url", + links: ["link1", "link2"], + tags: ["tag1", "tag2"], + metadata: { + level: "advanced", + duration: "2h", + }, + }); +}); + +test("validateTraining with partial raw data", () => { + const raw = { + title: "Partial Title", + metadata: { + level: "intermediate", + }, + }; + const result = validateTraining( + raw, + "partial-slug", + "Partial Content", + defaultTraining, + ); + expect(result).toEqual({ + type: "training", + title: "Partial Title", + description: "Default Description", + slug: "partial-slug", + content: "Partial Content", + author: "", + image: "", + links: [], + tags: [], + metadata: { + level: "intermediate", + duration: "1h", + }, + }); +}); + +test("validateTraining with empty raw data", () => { + const raw = {}; + const result = validateTraining( + raw, + "empty-slug", + "Empty Content", + defaultTraining, + ); + expect(result).toEqual({ + type: "training", + title: "Default Title", + description: "Default Description", + slug: "empty-slug", + content: "Empty Content", + author: "", + image: "", + links: [], + tags: [], + metadata: { + level: "beginner", + duration: "1h", + }, + }); +}); diff --git a/components/__tests__/utils.test.ts b/components/__tests__/utils.test.ts index 88a5de8..c45cf8e 100644 --- a/components/__tests__/utils.test.ts +++ b/components/__tests__/utils.test.ts @@ -1,5 +1,5 @@ import { test, expect } from "vitest"; -import { parseSlug } from "../src/mdxParser/contentParser"; +import { parseSlug } from "../src/mdxParser/mdxParsers"; test("parses basic slug correctly", () => { expect(parseSlug("simple")).toBe("simple"); diff --git a/components/index.ts b/components/index.ts index 3c1bd4f..d6e34c1 100644 --- a/components/index.ts +++ b/components/index.ts @@ -5,9 +5,9 @@ import { Footer } from "./src/footer"; import { CustomMDX } from "./src/mdx"; import { Navbar } from "./src/nav"; import { Form } from "./src/form"; -import { Post } from "./src/mdxParser/contentParser"; -import { getPosts } from "./src/mdxParser/contentParser"; -import { parseSlug } from "./src/mdxParser/contentParser"; +import { Post } from "./src/mdxParser/mdxParserTypes"; +import { getPosts } from "./src/mdxParser/mdxParsers"; +import { parseSlug } from "./src/mdxParser/mdxParsers"; export type { Post }; export { CustomMDX, Button, Section, Page, Footer, Navbar, Form }; diff --git a/components/src/mdxParser/mdxParserTypes.ts b/components/src/mdxParser/mdxParserTypes.ts new file mode 100644 index 0000000..8c5f5ce --- /dev/null +++ b/components/src/mdxParser/mdxParserTypes.ts @@ -0,0 +1,171 @@ + +// Constants +export const validMaturities = ["sandbox", "incubation", "graduated"] as const; +export type ContentType = "project" | "training" | "post" | "event"; + +// Base Types +interface BaseContent { + title?: string; + type: ContentType; +} + +interface WithDescription { + description: string; +} + +interface WithSlug { + slug: string; +} + +interface WithContent { + content: string; +} + +interface WithLinks { + links: Array<{ url: string; label: string }>; +} + +// Project Metadata +interface ProjectMetadata { + featured: boolean; + maturity: Maturity; +} + +export type Maturity = (typeof validMaturities)[number]; + +// Training Metadata +interface TrainingMetadata { + level?: string; + duration?: string; +} + +// Event Metadata +interface EventMetadata { + start: string; + duration?: Duration; + end?: string; + location?: string; + geo?: GeoLocation; + status?: EventStatus; + organizer?: Organizer; + url?: string; +} + +type EventStatus = "TENTATIVE" | "CONFIRMED" | "CANCELLED"; + +interface GeoLocation { + lat: number; + lon: number; +} + +interface Organizer { + name: string; + email: string; +} + +interface Duration { + seconds?: number; + minutes?: number; + hours?: number; + days?: number; + weeks?: number; +} + + +// Tag Types +export type TagCategory = "type" | "tool" | "cost" | "mode"; +export type TrainingTag = `${TagCategory}::${string}`; + +// Content Types +export interface Project extends BaseContent, WithDescription, WithSlug, WithContent, WithLinks { + type: "project"; + metadata: ProjectMetadata; +} + +export interface Training extends BaseContent, WithDescription, WithSlug, WithContent, WithLinks { + type: "training"; + author: string; + image: string; + tags: TrainingTag[]; + metadata: TrainingMetadata; +} + +//TODO: for the time being use custom post -> Should be replaced with the below interface +export interface Post { + metadata: Record; + slug: string; + content: string; + title?: string; + description?: string; +} + + +// export interface Post extends BaseContent, WithDescription, WithSlug, WithContent { +// type: "post"; +// metadata: Record; +// } + +export interface Event extends BaseContent, WithDescription, WithSlug, WithContent { + type: "event"; + metadata: EventMetadata; +} + +export type Content = Project | Training | Post | Event; + +// Defaults +export const contentDefaults = { + project: { + type: "project" as const, + title: "", + description: "", + slug: "", + content: "", + metadata: { + featured: false, + maturity: "sandbox" as Maturity, + }, + links: [], + }, + training: { + type: "training" as const, + title: "", + description: "", + slug: "", + content: "", + author: "", + image: "", + tags: [], + metadata: { + level: "beginner", + duration: "1h", + }, + }, + post: { + type: "post" as const, + title: "", + description: "", + slug: "", + content: "", + metadata: {}, + }, + event: { + type: "event" as const, + title: "", + description: "", + slug: "", + content: "", + metadata: { + start: new Date().toISOString(), + status: "TENTATIVE" as EventStatus, + }, + }, +}; export type ContentValidator = ( + raw: any, + slug: string, + content: string, + defaultContent: T +) => T; +export interface Parser { + (content: string, slug: string, metadata: unknown): T; +} + diff --git a/components/src/mdxParser/contentParser.ts b/components/src/mdxParser/mdxParsers.ts similarity index 53% rename from components/src/mdxParser/contentParser.ts rename to components/src/mdxParser/mdxParsers.ts index 42c32a6..bccbe64 100644 --- a/components/src/mdxParser/contentParser.ts +++ b/components/src/mdxParser/mdxParsers.ts @@ -2,7 +2,15 @@ import fs from "fs"; import path from "path"; import YAML from "yaml"; -import { Content, Project, Training, contentDefaults, ContentType } from "../types/parserTypes"; +import { + Content, + Project, + Training, + contentDefaults, + ContentType, + Parser, + Post, +} from "./mdxParserTypes"; import { validateMetadata } from "./mdxValidators"; export function parseSlug(fileBasename: string) { @@ -12,12 +20,13 @@ export function parseSlug(fileBasename: string) { } return fileBasename; } + function parseFrontmatter(content: string) { const match = /---\s*([\s\S]*?)\s*---/.exec(content); if (!match) return null; const yaml = match[1]; - const body = content.replace(match[0], '').trim(); + const body = content.replace(match[0], "").trim(); try { const data = YAML.parse(yaml); @@ -27,21 +36,31 @@ function parseFrontmatter(content: string) { } } -export function loadContent(dir: string, type: ContentType): T[] { +export function loadContent( + dir: string, + type: ContentType, +): T[] { const contentDir = path.join(process.cwd(), "content", dir); try { - return fs.readdirSync(contentDir) - .filter(file => path.extname(file) === ".mdx" && !file.startsWith('_')) - .map(file => { - const content = fs.readFileSync(path.join(contentDir, file), 'utf-8'); - const slug = parseSlug(path.basename(file, '.mdx')); + return fs + .readdirSync(contentDir) + .filter((file) => path.extname(file) === ".mdx" && !file.startsWith("_")) + .map((file) => { + const content = fs.readFileSync(path.join(contentDir, file), "utf-8"); + const slug = parseSlug(path.basename(file, ".mdx")); const parsed = parseFrontmatter(content); const defaultContent = contentDefaults[type] as T; return parsed - ? validateMetadata(parsed.data, slug, parsed.body, defaultContent, type) + ? validateMetadata( + parsed.data, + slug, + parsed.body, + defaultContent, + type, + ) : { ...defaultContent, slug }; }); } catch (error) { @@ -50,27 +69,24 @@ export function loadContent(dir: string, type: ContentType): } } -export const loadProjects = () => loadContent('projects', 'project') as Project[]; -export const loadTrainings = () => loadContent('trainings', 'training') as Training[]; -export const loadPosts = (dir: string) => loadContent(dir, 'post') as Post[]; +export const loadProjects = () => + loadContent("projects", "project") as Project[]; +export const loadTrainings = () => + loadContent("trainings", "training") as Training[]; -export type ContentValidator = ( - raw: any, - slug: string, - content: string, - defaultContent: T -) => T; +export const loadPosts = (dir: string) => loadContent(dir, "post") as Post[]; export function getMDXFiles(dir: string): string[] { try { - return fs.readdirSync(dir) - .filter(file => - path.extname(file) === ".mdx" && - !path.basename(file).startsWith("_") + return fs + .readdirSync(dir) + .filter( + (file) => + path.extname(file) === ".mdx" && !path.basename(file).startsWith("_"), ); } catch (error) { - console.error('Error reading directory:', error); + console.error("Error reading directory:", error); return []; } } @@ -78,7 +94,7 @@ export function getMDXFiles(dir: string): string[] { export function readFile( filePath: string, validationFn: Parser, - options = { encoding: 'utf-8' as const } + options = { encoding: "utf-8" as const }, ): T { try { const rawContent = fs.readFileSync(filePath, options); @@ -86,61 +102,54 @@ export function readFile( const { metadata, content } = parseMdxFile(rawContent); return validationFn(content, slug, metadata); } catch (error) { - console.error('Error reading file:', error); + console.error("Error reading file:", error); throw error; } } -export function parseMdxFile(fileContent: string): { metadata: Record; content: string; } { + +export function parseMdxFile(fileContent: string): { + metadata: Record; + content: string; +} { try { const frontmatterRegex = /---\s*([\s\S]*?)\s*---/; const match = frontmatterRegex.exec(fileContent); if (!match) { - throw new Error('No frontmatter found'); + throw new Error("No frontmatter found"); } const frontMatterBlock = match[1]; const content = fileContent.replace(frontmatterRegex, "").trim(); const metadata = YAML.parse(frontMatterBlock); - if (typeof metadata !== 'object' || metadata === null) { - throw new Error('Invalid frontmatter format'); + if (typeof metadata !== "object" || metadata === null) { + throw new Error("Invalid frontmatter format"); } return { metadata, content }; } catch (error) { - console.error('Error parsing MDX file:', error); + console.error("Error parsing MDX file:", error); return { metadata: {}, content: fileContent }; } } -export interface Parser { - (content: string, slug: string, metadata: unknown): T; -} -export interface Post { - metadata: Record; - slug: string; - content: string; - title?: string; - description?: string; -} export function getPosts(dir?: string): Post[] { const contentDir = path.join(process.cwd(), "content", dir || ""); try { const files = getMDXFiles(contentDir); - return files.map(file => readFile( - path.join(contentDir, file), - (content, slug, metadata) => ({ - metadata: metadata as Record, - slug, - content - }) - ) + return files.map((file) => + readFile( + path.join(contentDir, file), + (content, slug, metadata) => ({ + metadata: metadata as Record, + slug, + content, + }), + ), ); } catch (error) { - console.error('Error loading posts:', error); + console.error("Error loading posts:", error); return []; } } - - diff --git a/components/src/mdxParser/mdxValidators.ts b/components/src/mdxParser/mdxValidators.ts index dd46467..7ecedf1 100644 --- a/components/src/mdxParser/mdxValidators.ts +++ b/components/src/mdxParser/mdxValidators.ts @@ -1,18 +1,28 @@ -import { Training, Event, Project, ContentType, Maturity, validMaturities, Content } from "../types/parserTypes"; -import { ContentValidator } from "./contentParser"; +import { + Training, + Event, + Project, + ContentType, + Maturity, + validMaturities, + Content, + TrainingTag, +} from "./mdxParserTypes"; +import { ContentValidator } from "./mdxParserTypes"; export function validatePostMetadata(metadata: any) { return metadata || {}; /** * An object containing various metadata validation functions. - * + * * @property {Function} project - Validates project metadata. * @property {Function} training - Validates training metadata. * @property {Function} post - Validates post metadata. * @property {Function} event - Validates event metadata. */ -} export const validators = { +} +export const validators = { project: validateProjectMetadata, training: validateTrainingMetadata, post: validatePostMetadata, @@ -28,21 +38,26 @@ export function validatePostMetadata(metadata: any) { * @param defaultContent - The default content to fall back on if raw content is missing. * @returns The validated and structured training content. */ -const validateTraining: ContentValidator = (raw, slug, content, defaultContent) => ({ +export const validateTraining: ContentValidator = ( + raw, + slug, + content, + defaultContent, +) => ({ ...defaultContent, title: raw?.title || defaultContent.title, description: raw?.description || defaultContent.description, slug, content, - author: raw?.author || '', - image: raw?.image || '', + author: raw?.author || "", + image: raw?.image || "", links: raw?.links || [], tags: Array.isArray(raw?.tags) ? raw.tags : [], metadata: { ...defaultContent.metadata, - level: raw?.metadata?.level || 'beginner', - duration: raw?.metadata?.duration || '1h', - } + level: raw?.metadata?.level || "beginner", + duration: raw?.metadata?.duration || "1h", + }, }); export function validateTrainingMetadata(metadata: any) { @@ -58,7 +73,12 @@ export function validateTrainingMetadata(metadata: any) { * @param defaultContent - The default content to use if certain fields are missing in the raw data. * @returns A validated and structured Event object. */ -const validateEvent: ContentValidator = (raw, slug, content, defaultContent) => ({ +const validateEvent: ContentValidator = ( + raw, + slug, + content, + defaultContent, +) => ({ ...defaultContent, title: raw?.title || defaultContent.title, description: raw?.description || defaultContent.description, @@ -71,17 +91,16 @@ const validateEvent: ContentValidator = (raw, slug, content, defaultConte end: raw?.event?.end, location: raw?.event?.location, geo: raw?.event?.geo, - status: raw?.event?.status || 'TENTATIVE', + status: raw?.event?.status || "TENTATIVE", organizer: raw?.event?.organizer, - url: raw?.event?.url - } + url: raw?.event?.url, + }, }); export function validateEventMetadata(metadata: any) { return metadata || {}; } - /** * Validates and constructs a Project object by merging raw input data with default content. * @@ -91,7 +110,12 @@ export function validateEventMetadata(metadata: any) { * @param defaultContent - The default content to fall back on if raw data is incomplete. * @returns A validated and merged Project object. */ -const validateProject: ContentValidator = (raw, slug, content, defaultContent) => ({ +const validateProject: ContentValidator = ( + raw, + slug, + content, + defaultContent, +) => ({ ...defaultContent, title: raw?.title || defaultContent.title, description: raw?.description || defaultContent.description, @@ -101,8 +125,8 @@ const validateProject: ContentValidator = (raw, slug, content, defaultC metadata: { ...defaultContent.metadata, featured: raw?.metadata?.featured || false, - maturity: raw?.metadata?.maturity || 'sandbox', - } + maturity: raw?.metadata?.maturity || "sandbox", + }, }); export function validateProjectMetadata(metadata: any) { @@ -113,24 +137,26 @@ export function validateProjectMetadata(metadata: any) { } function parseMaturity(maturity: any): Maturity { - const cleanedMaturity = String(maturity || '').trim().toLowerCase(); + const cleanedMaturity = String(maturity || "") + .trim() + .toLowerCase(); return validMaturities.includes(cleanedMaturity as Maturity) ? (cleanedMaturity as Maturity) - : 'sandbox'; + : "sandbox"; } /** * A record of content validators for different content types. - * Each validator function takes raw content, a slug, processed content, + * Each validator function takes raw content, a slug, processed content, * and default content, and returns the validated content. - * + * * @type {Record>} - * + * * @property {ContentValidator} training - Validator for training content. * @property {ContentValidator} event - Validator for event content. * @property {ContentValidator} project - Validator for project content. * @property {ContentValidator} post - Validator for post content. - * + * * The `post` validator function: * @param {any} raw - The raw content data. * @param {string} slug - The slug for the content. @@ -144,22 +170,37 @@ export const contentValidators: Record> = { project: validateProject, post: (raw, slug, content, defaultContent) => ({ ...defaultContent, - title: raw?.title || '', - description: raw?.description || '', + title: raw?.title || "", + description: raw?.description || "", slug, content, - metadata: raw?.metadata || {} - }) + metadata: raw?.metadata || {}, + }), }; + +/** + * Validates the metadata of the given content. + * + * @template T - The type of the content. + * @param raw - The raw metadata to be validated. + * @param slug - The slug of the content. + * @param content - The content to be validated. + * @param defaultContent - The default content to fall back on if validation fails. + * @param type - The type of the content. + * @returns The validated content. + */ export function validateMetadata( raw: any, slug: string, content: string, defaultContent: T, - type: ContentType): T { + type: ContentType, +): T { const validator = contentValidators[type]; return validator(raw, slug, content, defaultContent); } - +export function isValidTrainingTag(tag: string): tag is TrainingTag { + return /^(type|tool|cost|mode)::.+$/.test(tag); +} diff --git a/components/src/mdxParser/projectMdxParser.ts b/components/src/mdxParser/projectMdxParser.ts deleted file mode 100644 index 3abb6f6..0000000 --- a/components/src/mdxParser/projectMdxParser.ts +++ /dev/null @@ -1,84 +0,0 @@ -// import path from "path"; -// import YAML from "yaml"; -// import { getMDXFiles, Post, readFile } from "./utils"; -// import { loadContent } from "./contentParser"; -// import { Project } from "./types/parserTypes"; - -// export type Maturity = typeof validMaturities[number]; - -// export interface MdxProject extends Post { -// title: string; -// description: string; -// metadata: { -// featured: boolean; -// maturity: Maturity; -// }; -// links: { -// url: string; -// label: string; -// }[]; -// } - -// export const validMaturities = ["sandbox", "incubation", "graduated"] as const; -// const defaultMaturity: Maturity = "sandbox"; - -// function parseMaturity(maturity: string): Maturity { -// const cleanedMaturity = maturity?.trim()?.toLowerCase(); -// return validMaturities.includes(cleanedMaturity as Maturity) -// ? (cleanedMaturity as Maturity) -// : defaultMaturity; -// } - -// function projectParser(fileContent: string, slug: string, rawMetadata: any): MdxProject { -// try { -// console.log('Raw metadata before validation:', rawMetadata); -// return validateProject(fileContent, slug, rawMetadata); -// } catch (error) { -// console.error('Error parsing project:', error); -// return validateProject(fileContent, slug, {}); -// } -// } - -// function validateProject(content: string, slug: string, metadata: any): MdxProject { -// const defaultProject: MdxProject = { -// title: "", -// description: "", -// slug, -// metadata: { -// featured: false, -// maturity: defaultMaturity -// }, -// links: [], -// content: "" -// }; - -// // Handle both nested and flat metadata structures -// const metadataFields = metadata.metadata || metadata; -// const featured = metadataFields.featured ?? metadata.featured ?? defaultProject.metadata.featured; -// const maturity = parseMaturity(metadataFields.maturity ?? metadata.maturity); - -// return { -// title: metadata.title || defaultProject.title, -// description: metadata.description || defaultProject.description, -// slug: slug || defaultProject.slug, -// metadata: { -// featured, -// maturity -// }, -// links: Array.isArray(metadata.links) ? metadata.links : defaultProject.links, -// content: content || defaultProject.content -// }; -// } - -// // export function loadProjects(dir?: string): MdxProject[] { -// // const contentDir = path.join(process.cwd(), "content", dir || ""); -// // try { -// // const files = getMDXFiles(contentDir); -// // return files.map(file => readFile(path.join(contentDir, file), projectParser)); -// // } catch (error) { -// // console.error('Error reading projects:', error); -// // return []; -// // } -// // } - -// // export const loadProjects = () => loadContent('projects', 'project') as Project[]; \ No newline at end of file diff --git a/components/src/partials/events.tsx b/components/src/partials/events.tsx index fc03c61..da2075f 100644 --- a/components/src/partials/events.tsx +++ b/components/src/partials/events.tsx @@ -1,4 +1,4 @@ -import { getPosts } from "../mdxParser/contentParser"; +import { getPosts } from "../mdxParser/mdxParsers"; import { Card } from "../card"; export function EventsPartial({ showPast = false }: { showPast?: boolean }) { diff --git a/components/src/partials/projects.tsx b/components/src/partials/projects.tsx index b65c82e..2e6970e 100644 --- a/components/src/partials/projects.tsx +++ b/components/src/partials/projects.tsx @@ -1,6 +1,6 @@ import { Card } from "../card"; import { Button } from "../button"; -import { loadProjects } from "../mdxParser/contentParser"; +import { loadProjects } from "../mdxParser/mdxParsers"; function getRandomItems(array: T[], numItems: number): T[] { const shuffled = array.sort(() => 0.5 - Math.random()); diff --git a/components/src/types/parserTypes.ts b/components/src/types/parserTypes.ts deleted file mode 100644 index c2071f2..0000000 --- a/components/src/types/parserTypes.ts +++ /dev/null @@ -1,138 +0,0 @@ -// types.ts -export const validMaturities = ["sandbox", "incubation", "graduated"] as const; -export type Maturity = typeof validMaturities[number]; -export type ContentType = 'project' | 'training' | 'post' | 'event'; - -interface BaseContent { - title: string; - description: string; - slug: string; - content: string; -} - -export interface Project extends BaseContent { - type: 'project'; - metadata: { - featured: boolean; - maturity: Maturity; - }; - links: Array<{ url: string; label: string }>; -} - -export type TagCategory = 'type' | 'tool' | 'cost' | 'mode'; - -export function isValidTrainingTag(tag: string): tag is TrainingTag { - return /^(type|tool|cost|mode)::.+$/.test(tag); -} - -export type TagValue = string; -export type TrainingTag = `${TagCategory}::${TagValue}`; - -export interface Training extends BaseContent { - type: 'training'; - title: string; - description: string; - author: string; - image: string; - links: { - url: string; - label: string; - }[]; - tags: TrainingTag[]; - metadata: { - level?: string; - duration?: string; - }; -} - -export interface Post extends BaseContent { - type: 'post'; - metadata: Record; -} - -export type Content = Project | Training | Post | Event; - -// contentDefaults in contentParser.ts or a separate file -export const contentDefaults = { - project: { - type: 'project', - title: '', - description: '', - slug: '', - content: '', - metadata: { - featured: false, - maturity: 'sandbox', - }, - links: [], - }, - training: { - type: 'training', - title: '', - description: '', - slug: '', - content: '', - metadata: { - level: 'beginner', - duration: '1h', - }, - }, - post: { - type: 'post', - title: '', - description: '', - slug: '', - content: '', - metadata: {}, - }, - event: { - type: 'event' as const, - title: '', - description: '', - slug: '', - content: '', - metadata: { - start: new Date().toISOString(), - status: 'TENTATIVE' as EventStatus - } - } -}; - - -// parserTypes.ts - -interface GeoLocation { - lat: number; - lon: number; -} - -interface Organizer { - name: string; - email: string; -} - -interface Duration { - seconds?: number; - minutes?: number; - hours?: number; - days?: number; - weeks?: number; -} - -type EventStatus = 'TENTATIVE' | 'CONFIRMED' | 'CANCELLED'; - -interface EventMetadata { - start: string; // ISO date string - duration?: Duration; - end?: string; // ISO date string - location?: string; - geo?: GeoLocation; - status?: EventStatus; - organizer?: Organizer; - url?: string; -} - -export interface Event extends BaseContent { - type: 'event'; - metadata: EventMetadata; -} diff --git a/components/src/utils.ts b/components/src/utils.ts index 139597f..e69de29 100644 --- a/components/src/utils.ts +++ b/components/src/utils.ts @@ -1,2 +0,0 @@ - - diff --git a/content/events/20250202-aec-hackathon-zurich-2025.mdx b/content/events/20250202-aec-hackathon-zurich-2025.mdx index 8837dfa..0cf0441 100644 --- a/content/events/20250202-aec-hackathon-zurich-2025.mdx +++ b/content/events/20250202-aec-hackathon-zurich-2025.mdx @@ -12,10 +12,10 @@ links: ![AEC Hackathon - Zurich Edition](/images/events/20250207_AEC-Hackathon-ZurichV2.png "AEC Hackathon - Zurich Edition") -Our first hackathon in Zurich was a huge success with over 250 attendees. We are super happy that we managed to bring the same energy and innovation to our recent Hackathon in Munich, organised together with the TUM Venture Lab. +Our first hackathon in Zurich was a huge success with over 250 attendees. We are super happy that we managed to bring the same energy and innovation to our recent Hackathon in Munich, organised together with the TUM Venture Lab. And now it's time to roll up our sleeves again and get to work in Zurich! -This time, the [Institute for Digital Construction and Wood Industry](https://www.bfh.ch/en/research/research-areas/institute-digital-construction-wood-industry/) from Berner Fachhochschule, led by Katharina Lindenberg, will join us as Key Partner for the Friday session. In this session, we will investigate the potential for code-based collaboration in the wood industry. Can this kind of collaboration help to secure the technological advantages of the swiss timber industry? Come by and find out with us! +This time, the [Institute for Digital Construction and Wood Industry](https://www.bfh.ch/en/research/research-areas/institute-digital-construction-wood-industry/) from Berner Fachhochschule, led by Katharina Lindenberg, will join us as Key Partner for the Friday session. In this session, we will investigate the potential for code-based collaboration in the wood industry. Can this kind of collaboration help to secure the technological advantages of the swiss timber industry? Come by and find out with us! From February 07-09, 2025, expect a thrilling event where the brightest minds in Architecture, Engineering, and Construction come together to innovate and collaborate. It will be a weekend filled with creative problem-solving, hands-on workshops, and opportunities to network with industry leaders and experts committed to driving innovation in the AEC sector. @@ -30,14 +30,13 @@ Whether you're a seasoned professional or a passionate student, this hackathon i ## Organiser -This event is proudly organised by opensource.construction together with ZHAW Winterthur (location host) and the Institute for Digital Construction and Wood Industry from Berner Fachhochschule (key partner). +This event is proudly organised by opensource.construction together with ZHAW Winterthur (location host) and the Institute for Digital Construction and Wood Industry from Berner Fachhochschule (key partner). ## Location

The event will take place in Halle 180 from ZHAW in Winterthur.

Exact adress: Tössfeldstrasse 11 / 8400 Winterthur – Switzerland - ## Date February 07-09, 2025 @@ -45,48 +44,52 @@ February 07-09, 2025 ## Event Agenda (preliminary)

The event will be kicked-off with a symposium on Friday afternoon.

-

The Hackathon itself starts on Friday evening 17:30 and ends Sunday at around 17:00.

- +

+ The Hackathon itself starts on Friday evening 17:30 and ends Sunday at around + 17:00. +

## Day 1: Friday, February 02 -- 13:00 Doors open +- 13:00 Doors open - **13:30 – 14:00 Welcome from the organisers: The potential of code-based collaboration for the Swiss Wood Industry** - **14:00 – 15:15 Insights into the practice of a digital wood industry** + - We will hear insights from J. Wiesinger (Gropyus), E. Augustynowicz (BFH) and companies like Cadwork, Technowood and others. Stay tuned, we will publish more details soon. - 15:15 - 15:30 Coffee Break - **15:30 - 16:15 Break-out sessions > learn hands-on with the experts** + - LCA and data availability (L. Trümpler, LT+ / H. Schmid, lignum) - Knowledge Bases: Materialarchiv, Holzbaukultur.ch, Prix Lignum Archiv (ZHAW) - Prefab Construction Supply Chain (Y. Moradi, MOD) - What’s the missing part for enabling automated planning processes in timber buildings? (J. Wiesinger, gropyus) - Open Source as a driver to secure the locational advantages of Swiss Timber Industry (M. Vomhof, opensource.construction) -- 16:15 - 16:30 Coffee Break +- 16:15 - 16:30 Coffee Break -- **16:30 - 17:15 Presentation & Discussion Break-out session** +- **16:30 - 17:15 Presentation & Discussion Break-out session** **>>> START HACKATHON <<<** -- 17:30 – 18:15 Welcome, Intro to the Hackathon and presentation of Partner Challenges -- 18:15 – 21:00 Team Formation and Brainstorming Sessions +- 17:30 – 18:15 Welcome, Intro to the Hackathon and presentation of Partner Challenges +- 18:15 – 21:00 Team Formation and Brainstorming Sessions ## Day 2: Saturday, February 03 -- 09:00 – 12:00 Hacking -- 12:00 – 13:00 Lunch Break -- 13:00 – 18:00 Hacking and Tech-Talks -- 18:00 – 20:00 Dinner and Networking +- 09:00 – 12:00 Hacking +- 12:00 – 13:00 Lunch Break +- 13:00 – 18:00 Hacking and Tech-Talks +- 18:00 – 20:00 Dinner and Networking ## Day 3: Sunday, February 04 -- 09:00 – 12:00 Final Hacking Session -- 12:00 – 13:00 Lunch Break -- 13:00 – 16:00 Project Presentations and Judging -- 16:00 – 17:00 Award Ceremony -- 17:00 – 18:00 Closing Remarks and Networking +- 09:00 – 12:00 Final Hacking Session +- 12:00 – 13:00 Lunch Break +- 13:00 – 16:00 Project Presentations and Judging +- 16:00 – 17:00 Award Ceremony +- 17:00 – 18:00 Closing Remarks and Networking ## Contact Us diff --git a/content/projects/_xeokit.mdx b/content/projects/_xeokit.mdx index 1372acb..fda136e 100644 --- a/content/projects/_xeokit.mdx +++ b/content/projects/_xeokit.mdx @@ -7,6 +7,7 @@ links: - url: https://github.com/xeokit label: GitHub --- + xeokit is an open source 3D graphics SDK from xeolabs for BIM and AEC. Built to view huge models in the browser. Used by industry leaders. @@ -15,33 +16,42 @@ Please use the following text as a reference to describe your project (or simply Thanks! ### Problem + Please describe in a few sentences what problem your solution addresses. ### Solution + Please describe in a few sentences how you approach this problem with your solution. ### Why Open Source? + Please describe in a few sentences why your solution is open source. ### Technology + Please describe in a few bullet points how your tech stack looks like. ### License + What licence(s) do you use? ### Operating Model + How do you maintain and update your solution? Who contributes to your solution (team members of your companies / users and other volunteers / …) Who are your typical customers? How do you earn money? ### About the team + Please describe in a few sentences who are the key people behind the project. ### Contact + For more info, please reach out to: person xyz ### Image / Video Footage + Please insert download link(s) here: Link1 Link2 diff --git a/content/trainings/python_zurich.mdx b/content/trainings/python_zurich.mdx index 84d9b56..0a8834c 100644 --- a/content/trainings/python_zurich.mdx +++ b/content/trainings/python_zurich.mdx @@ -131,4 +131,4 @@ I'm an architectural engineer, turned full-stack developer. I have for the last 6+ years been working with developing software for engineers and architects, both in-house and as a consultant. I have extensive experience with creating Revit and Grasshopper plugins, building web apps, building physics and LCA. -The course is hosted by [Christian Kongsgaard ApS](https://kongsgaard.eu) \ No newline at end of file +The course is hosted by [Christian Kongsgaard ApS](https://kongsgaard.eu) From d349d3b7c556191f1f237fadaf1c6ece6c2abb5e Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:37:07 +0100 Subject: [PATCH 04/33] refactor: update mdxParser imports and enhance event loading functionality --- app/(single-page)/[pageType]/[slug]/page.tsx | 8 +- app/projects/page.tsx | 6 +- components/src/mdxParser/mdxParserTypes.ts | 65 +++++++------- components/src/mdxParser/mdxParsers.ts | 11 ++- components/src/mdxParser/mdxValidators.ts | 90 +++++++++++++------- components/src/partials/events.tsx | 39 +++------ 6 files changed, 121 insertions(+), 98 deletions(-) diff --git a/app/(single-page)/[pageType]/[slug]/page.tsx b/app/(single-page)/[pageType]/[slug]/page.tsx index 0a63c27..cf3d7af 100644 --- a/app/(single-page)/[pageType]/[slug]/page.tsx +++ b/app/(single-page)/[pageType]/[slug]/page.tsx @@ -2,8 +2,8 @@ import { loadPosts, loadProjects, loadTrainings, -} from "@opensource-construction/components/src/mdxParser/contentParser"; -import { Page, getPosts } from "@opensource-construction/components"; +} from "@/components/src/mdxParser/mdxParsers"; +import { Page } from "@opensource-construction/components"; import { notFound } from "next/navigation"; type PageType = "events" | "projects" | "trainings" | "faqs" | "page"; @@ -20,13 +20,13 @@ export async function generateStaticParams() { ...loadProjects().map((p) => { return { slug: p.slug, pageType: "projects" as PageType }; }), - ...getPosts("events").map((p) => { + ...loadPosts("events").map((p) => { return { slug: p.slug, pageType: "events" as PageType }; }), ...loadTrainings().map((p) => { return { slug: p.slug, pageType: "trainings" as PageType }; }), - ...getPosts("page").map((p) => { + ...loadPosts("page").map((p) => { return { slug: p.slug, pageType: "page" as PageType }; }), ]; diff --git a/app/projects/page.tsx b/app/projects/page.tsx index 871665f..51c5cbc 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -1,10 +1,10 @@ import { Button, Section } from "@/components"; -import { loadProjects } from "@opensource-construction/components/src/mdxParser/contentParser"; +import { loadProjects } from "@/components/src/mdxParser/mdxParsers"; import { Maturity, Project, validMaturities, -} from "@opensource-construction/components/src/mdxParser/parserTypes"; +} from "@/components/src/mdxParser/mdxParserTypes"; function capitalizeFirstLetter(string: string): string { return string.charAt(0).toUpperCase() + string.slice(1); @@ -15,7 +15,7 @@ export default function Projects() { const projectsByMaturity = projects.reduce>( (acc, project) => { - const maturity = project.metadata.maturity; // Access maturity through project.project + const maturity = project.metadata.maturity; if (!acc[maturity]) { acc[maturity] = []; } diff --git a/components/src/mdxParser/mdxParserTypes.ts b/components/src/mdxParser/mdxParserTypes.ts index 8c5f5ce..3ed3400 100644 --- a/components/src/mdxParser/mdxParserTypes.ts +++ b/components/src/mdxParser/mdxParserTypes.ts @@ -3,6 +3,9 @@ export const validMaturities = ["sandbox", "incubation", "graduated"] as const; export type ContentType = "project" | "training" | "post" | "event"; +export type Content = Project | Training | Post | Event; + + // Base Types interface BaseContent { title?: string; @@ -25,7 +28,12 @@ interface WithLinks { links: Array<{ url: string; label: string }>; } -// Project Metadata +// Project +export interface Project extends BaseContent, WithDescription, WithSlug, WithContent, WithLinks { + type: "project"; + metadata: ProjectMetadata; +} + interface ProjectMetadata { featured: boolean; maturity: Maturity; @@ -33,13 +41,26 @@ interface ProjectMetadata { export type Maturity = (typeof validMaturities)[number]; -// Training Metadata +// Training +export interface Training extends BaseContent, WithDescription, WithSlug, WithContent, WithLinks { + type: "training"; + author: string; + image: string; + tags: TrainingTag[]; + metadata: TrainingMetadata; +} + interface TrainingMetadata { level?: string; duration?: string; } -// Event Metadata +// Event +export interface Event extends BaseContent, WithDescription, WithSlug, WithContent { + type: "event"; + metadata: EventMetadata; +} + interface EventMetadata { start: string; duration?: Duration; @@ -49,6 +70,8 @@ interface EventMetadata { status?: EventStatus; organizer?: Organizer; url?: string; + isPast: boolean; + startDate: Date; } type EventStatus = "TENTATIVE" | "CONFIRMED" | "CANCELLED"; @@ -71,24 +94,10 @@ interface Duration { weeks?: number; } - -// Tag Types export type TagCategory = "type" | "tool" | "cost" | "mode"; export type TrainingTag = `${TagCategory}::${string}`; -// Content Types -export interface Project extends BaseContent, WithDescription, WithSlug, WithContent, WithLinks { - type: "project"; - metadata: ProjectMetadata; -} -export interface Training extends BaseContent, WithDescription, WithSlug, WithContent, WithLinks { - type: "training"; - author: string; - image: string; - tags: TrainingTag[]; - metadata: TrainingMetadata; -} //TODO: for the time being use custom post -> Should be replaced with the below interface export interface Post { @@ -99,19 +108,11 @@ export interface Post { description?: string; } - // export interface Post extends BaseContent, WithDescription, WithSlug, WithContent { // type: "post"; // metadata: Record; // } -export interface Event extends BaseContent, WithDescription, WithSlug, WithContent { - type: "event"; - metadata: EventMetadata; -} - -export type Content = Project | Training | Post | Event; - // Defaults export const contentDefaults = { project: { @@ -132,10 +133,10 @@ export const contentDefaults = { description: "", slug: "", content: "", - author: "", - image: "", - tags: [], metadata: { + author: "", + image: "", + tags: [], level: "beginner", duration: "1h", }, @@ -155,8 +156,14 @@ export const contentDefaults = { slug: "", content: "", metadata: { - start: new Date().toISOString(), status: "TENTATIVE" as EventStatus, + startDate: new Date(), + start: Intl.DateTimeFormat("en-UK", { + dateStyle: "short", + timeStyle: "short", + timeZone: "Europe/Zurich", + }).format(new Date()), + isPast: false, }, }, }; export type ContentValidator = ( diff --git a/components/src/mdxParser/mdxParsers.ts b/components/src/mdxParser/mdxParsers.ts index bccbe64..a4750cb 100644 --- a/components/src/mdxParser/mdxParsers.ts +++ b/components/src/mdxParser/mdxParsers.ts @@ -10,6 +10,7 @@ import { ContentType, Parser, Post, + Event } from "./mdxParserTypes"; import { validateMetadata } from "./mdxValidators"; @@ -61,7 +62,7 @@ export function loadContent( defaultContent, type, ) - : { ...defaultContent, slug }; + : { ...defaultContent, slug } as unknown as T; // Changed 'as T' to 'as unknown as T' }); } catch (error) { console.error(`Error loading ${type}:`, error); @@ -70,10 +71,14 @@ export function loadContent( } export const loadProjects = () => - loadContent("projects", "project") as Project[]; + loadContent("projects", "project"); export const loadTrainings = () => - loadContent("trainings", "training") as Training[]; + loadContent("trainings", "training"); + +export const loadEvents = (): Event[] => { + return loadContent("events", "event"); +}; export const loadPosts = (dir: string) => loadContent(dir, "post") as Post[]; diff --git a/components/src/mdxParser/mdxValidators.ts b/components/src/mdxParser/mdxValidators.ts index 7ecedf1..02ddeac 100644 --- a/components/src/mdxParser/mdxValidators.ts +++ b/components/src/mdxParser/mdxValidators.ts @@ -12,16 +12,8 @@ import { ContentValidator } from "./mdxParserTypes"; export function validatePostMetadata(metadata: any) { return metadata || {}; - - /** - * An object containing various metadata validation functions. - * - * @property {Function} project - Validates project metadata. - * @property {Function} training - Validates training metadata. - * @property {Function} post - Validates post metadata. - * @property {Function} event - Validates event metadata. - */ } + export const validators = { project: validateProjectMetadata, training: validateTrainingMetadata, @@ -49,14 +41,14 @@ export const validateTraining: ContentValidator = ( description: raw?.description || defaultContent.description, slug, content, - author: raw?.author || "", - image: raw?.image || "", - links: raw?.links || [], - tags: Array.isArray(raw?.tags) ? raw.tags : [], metadata: { ...defaultContent.metadata, level: raw?.metadata?.level || "beginner", duration: raw?.metadata?.duration || "1h", + author: raw?.author || "", + image: raw?.image || "", + links: raw?.links || [], + tags: Array.isArray(raw?.tags) ? raw.tags : [], }, }); @@ -73,32 +65,64 @@ export function validateTrainingMetadata(metadata: any) { * @param defaultContent - The default content to use if certain fields are missing in the raw data. * @returns A validated and structured Event object. */ -const validateEvent: ContentValidator = ( +export const validateEvent: ContentValidator = ( raw, slug, content, defaultContent, -) => ({ - ...defaultContent, - title: raw?.title || defaultContent.title, - description: raw?.description || defaultContent.description, - slug, - content, - metadata: { - ...defaultContent.metadata, - start: raw?.event?.start || new Date().toISOString(), - duration: raw?.event?.duration, - end: raw?.event?.end, - location: raw?.event?.location, - geo: raw?.event?.geo, - status: raw?.event?.status || "TENTATIVE", - organizer: raw?.event?.organizer, - url: raw?.event?.url, - }, -}); +) => { + const eventStart = new Date(raw?.event?.start || defaultContent.metadata.start); + const isPast = eventStart.getTime() < Date.now(); + const formattedStart = Intl.DateTimeFormat("en-UK", { + dateStyle: "short", + timeStyle: "short", + timeZone: "Europe/Zurich", + }).format(eventStart); + + return { + ...defaultContent, + type: "event", + title: raw?.title || defaultContent.title, + description: raw?.description || defaultContent.description, + slug, + content, + metadata: { + ...defaultContent.metadata, + duration: raw?.event?.duration || { hours: 1 }, + location: raw?.event?.location, + geo: raw?.event?.geo, + status: raw?.event?.status || "TENTATIVE", + organizer: raw?.event?.organizer, + url: raw?.event?.url, + start: formattedStart, + isPast: isPast, + }, + }; +}; export function validateEventMetadata(metadata: any) { - return metadata || {}; + return { + duration: metadata?.duration, + end: metadata?.end, + location: metadata?.location, + geo: metadata?.geo, + status: metadata?.status || "TENTATIVE", + organizer: metadata?.organizer, + url: metadata?.url, + isPast: new Date(metadata?.start).getTime() < Date.now(), + startDate: metadata?.start ? new Date(metadata.start) : new Date(), + start: metadata?.start + ? Intl.DateTimeFormat("en-UK", { + dateStyle: "short", + timeStyle: "short", + timeZone: "Europe/Zurich", + }).format(new Date(metadata.start)) + : Intl.DateTimeFormat("en-UK", { + dateStyle: "short", + timeStyle: "short", + timeZone: "Europe/Zurich", + }).format(new Date()), + }; } /** diff --git a/components/src/partials/events.tsx b/components/src/partials/events.tsx index da2075f..99dd811 100644 --- a/components/src/partials/events.tsx +++ b/components/src/partials/events.tsx @@ -1,40 +1,27 @@ -import { getPosts } from "../mdxParser/mdxParsers"; import { Card } from "../card"; +import { loadEvents } from "../mdxParser/mdxParsers"; export function EventsPartial({ showPast = false }: { showPast?: boolean }) { - let events = getPosts("events"); + let eventsParsed = loadEvents(); - let parsedEvents = events - .map((e) => { - let event = e.metadata.event; - let eventStart = new Date(event.start); - - event.title = e.metadata.title; - event.slug = e.slug; - - event.startDate = eventStart; - event.start = Intl.DateTimeFormat("en-UK", { - dateStyle: "short", - timeZone: "Europe/Zurich", - }).format(new Date(eventStart)); - event.isPast = eventStart.getTime() < Date.now(); - - return event; - }) - .sort((a, b) => (a.startDate < b.startDate ? 1 : -1)) - .filter((e) => (e.isPast && showPast) || (!e.isPast && !showPast)); + let es = eventsParsed + .sort((a, b) => (a.metadata.startDate < b.metadata.startDate ? 1 : -1)) + .filter( + (e) => + (e.metadata.isPast && showPast) || (!e.metadata.isPast && !showPast), + ); return (
- {!parsedEvents.length + {!es.length ? "No pending events" - : parsedEvents.map((e) => ( + : es.map((e) => ( ))} From 51543c16785903438bbc3a68d136194c8e209169 Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Fri, 13 Dec 2024 07:58:02 +0100 Subject: [PATCH 05/33] refactor: enhance content loading and validation for new page type in mdxParser --- app/(single-page)/[pageType]/[slug]/page.tsx | 3 +- components/src/mdxParser/mdxParserTypes.ts | 78 +++++++++++++++----- components/src/mdxParser/mdxParsers.ts | 32 +++++--- components/src/mdxParser/mdxValidators.ts | 40 ++++++---- 4 files changed, 110 insertions(+), 43 deletions(-) diff --git a/app/(single-page)/[pageType]/[slug]/page.tsx b/app/(single-page)/[pageType]/[slug]/page.tsx index cf3d7af..89d483a 100644 --- a/app/(single-page)/[pageType]/[slug]/page.tsx +++ b/app/(single-page)/[pageType]/[slug]/page.tsx @@ -1,4 +1,5 @@ import { + loadEvents, loadPosts, loadProjects, loadTrainings, @@ -20,7 +21,7 @@ export async function generateStaticParams() { ...loadProjects().map((p) => { return { slug: p.slug, pageType: "projects" as PageType }; }), - ...loadPosts("events").map((p) => { + ...loadEvents().map((p) => { return { slug: p.slug, pageType: "events" as PageType }; }), ...loadTrainings().map((p) => { diff --git a/components/src/mdxParser/mdxParserTypes.ts b/components/src/mdxParser/mdxParserTypes.ts index 3ed3400..fbf89f6 100644 --- a/components/src/mdxParser/mdxParserTypes.ts +++ b/components/src/mdxParser/mdxParserTypes.ts @@ -1,14 +1,14 @@ // Constants export const validMaturities = ["sandbox", "incubation", "graduated"] as const; -export type ContentType = "project" | "training" | "post" | "event"; +export type ContentType = "project" | "training" | "post" | "event" | "page"; -export type Content = Project | Training | Post | Event; +export type Content = Project | Training | Post | Event | Page; // Base Types interface BaseContent { - title?: string; + title: string; type: ContentType; } @@ -29,12 +29,12 @@ interface WithLinks { } // Project -export interface Project extends BaseContent, WithDescription, WithSlug, WithContent, WithLinks { +export interface Project extends BaseContent, WithDescription, WithSlug, WithContent { type: "project"; metadata: ProjectMetadata; } -interface ProjectMetadata { +interface ProjectMetadata extends WithLinks { featured: boolean; maturity: Maturity; } @@ -42,17 +42,17 @@ interface ProjectMetadata { export type Maturity = (typeof validMaturities)[number]; // Training -export interface Training extends BaseContent, WithDescription, WithSlug, WithContent, WithLinks { +export interface Training extends BaseContent, WithDescription, WithSlug, WithContent { type: "training"; - author: string; - image: string; - tags: TrainingTag[]; metadata: TrainingMetadata; } -interface TrainingMetadata { +interface TrainingMetadata extends WithLinks { level?: string; duration?: string; + author: string; + image: string; + tags: TrainingTag[]; } // Event @@ -97,7 +97,26 @@ interface Duration { export type TagCategory = "type" | "tool" | "cost" | "mode"; export type TrainingTag = `${TagCategory}::${string}`; +export interface Page extends BaseContent, WithContent { + type: "page"; + metadata: PageMetadata; +} + +interface PageMetadata extends WithLinks { + event?: object; + project?: object; +} + +// metadata: { +// title: string; +// event?: object; +// project?: object; +// links?: { +// url: string; +// label: string; +// }[]; +// }; //TODO: for the time being use custom post -> Should be replaced with the below interface export interface Post { @@ -113,23 +132,38 @@ export interface Post { // metadata: Record; // } +type ContentDefaults = { + [K in ContentType]: DefaultContent; +}; + +type DefaultContent = T extends "project" + ? Omit & { type: "project" } + : T extends "training" + ? Omit & { type: "training" } + : T extends "event" + ? Omit & { type: "event" } + : T extends "page" + ? Omit & { type: "page" } + : T extends "post" + ? Omit & { type: "post" } + : never; + + // Defaults -export const contentDefaults = { +export const contentDefaults: ContentDefaults = { project: { type: "project" as const, - title: "", description: "", slug: "", content: "", metadata: { featured: false, maturity: "sandbox" as Maturity, + links: [], }, - links: [], }, training: { type: "training" as const, - title: "", description: "", slug: "", content: "", @@ -139,11 +173,11 @@ export const contentDefaults = { tags: [], level: "beginner", duration: "1h", + links: [], }, }, post: { type: "post" as const, - title: "", description: "", slug: "", content: "", @@ -151,7 +185,6 @@ export const contentDefaults = { }, event: { type: "event" as const, - title: "", description: "", slug: "", content: "", @@ -166,7 +199,18 @@ export const contentDefaults = { isPast: false, }, }, -}; export type ContentValidator = ( + page: { + type: "page" as const, + content: "", + metadata: { + links: [], + event: {}, + project: {}, + }, + } +}; + +export type ContentValidator = ( raw: any, slug: string, content: string, diff --git a/components/src/mdxParser/mdxParsers.ts b/components/src/mdxParser/mdxParsers.ts index a4750cb..2ee2d29 100644 --- a/components/src/mdxParser/mdxParsers.ts +++ b/components/src/mdxParser/mdxParsers.ts @@ -70,17 +70,6 @@ export function loadContent( } } -export const loadProjects = () => - loadContent("projects", "project"); - -export const loadTrainings = () => - loadContent("trainings", "training"); - -export const loadEvents = (): Event[] => { - return loadContent("events", "event"); -}; - -export const loadPosts = (dir: string) => loadContent(dir, "post") as Post[]; export function getMDXFiles(dir: string): string[] { try { @@ -139,6 +128,10 @@ export function parseMdxFile(fileContent: string): { } } + +/** + * @deprecated Use specified `loaders` instead. + */ export function getPosts(dir?: string): Post[] { const contentDir = path.join(process.cwd(), "content", dir || ""); try { @@ -158,3 +151,20 @@ export function getPosts(dir?: string): Post[] { return []; } } + +//Loaders + +export const loadProjects = () => + loadContent("projects", "project"); + +export const loadTrainings = () => + loadContent("trainings", "training"); + +export const loadEvents = (): Event[] => { + return loadContent("events", "event"); +}; + +export const loadPages = () => loadContent("page", "page") as Post[]; + +export const loadPosts = (dir: string) => loadContent(dir, "post") as Post[]; + diff --git a/components/src/mdxParser/mdxValidators.ts b/components/src/mdxParser/mdxValidators.ts index 02ddeac..ca4c059 100644 --- a/components/src/mdxParser/mdxValidators.ts +++ b/components/src/mdxParser/mdxValidators.ts @@ -7,6 +7,7 @@ import { validMaturities, Content, TrainingTag, + Page, } from "./mdxParserTypes"; import { ContentValidator } from "./mdxParserTypes"; @@ -19,6 +20,7 @@ export const validators = { training: validateTrainingMetadata, post: validatePostMetadata, event: validateEventMetadata, + page: validatePageMetadata, }; /** @@ -169,24 +171,33 @@ function parseMaturity(maturity: any): Maturity { : "sandbox"; } +const validatePage: ContentValidator = ( + raw, + slug, + content, + defaultContent, +) => ({ + ...defaultContent, + title: raw?.title || defaultContent.title, + content, + metadata: validatePageMetadata(raw?.metadata), + slug, + +}); + +function validatePageMetadata(metadata: any) { + return { + ...metadata, + links: metadata?.links || [], + events: metadata?.events || [], + projects: metadata?.projects || [], + } +} + /** * A record of content validators for different content types. * Each validator function takes raw content, a slug, processed content, * and default content, and returns the validated content. - * - * @type {Record>} - * - * @property {ContentValidator} training - Validator for training content. - * @property {ContentValidator} event - Validator for event content. - * @property {ContentValidator} project - Validator for project content. - * @property {ContentValidator} post - Validator for post content. - * - * The `post` validator function: - * @param {any} raw - The raw content data. - * @param {string} slug - The slug for the content. - * @param {any} content - The processed content. - * @param {any} defaultContent - The default content structure. - * @returns {any} - The validated content with title, description, slug, content, and metadata. */ export const contentValidators: Record> = { training: validateTraining, @@ -200,6 +211,7 @@ export const contentValidators: Record> = { content, metadata: raw?.metadata || {}, }), + page: validatePage, }; /** From c9559236462288da45dbf053c1439579449c2071 Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Fri, 13 Dec 2024 07:58:43 +0100 Subject: [PATCH 06/33] Adding zod to the project for mdx validation --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 52b41dc..1c4b2e6 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "react-icons": "^5.2.1", "slugify": "^1.6.6", "yaml": "^2.4.2", - "zod": "^3.23.8" + "zod": "^3.24.0" }, "devDependencies": { "@chromatic-com/storybook": "^1.5.0", From 9a2647c0013999becc6e9fc5dec6181aa12cf6de Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Fri, 13 Dec 2024 10:51:52 +0100 Subject: [PATCH 07/33] refactor: update mdxParser- simplify & refactor --- app/(single-page)/[pageType]/[slug]/page.tsx | 70 +++- app/actions.ts | 1 - app/clusters/page.tsx | 1 + app/projects/page.tsx | 12 +- app/trainings/page.tsx | 12 +- components/__tests__/mdxValidators.test.ts | 4 +- components/index.ts | 4 +- components/src/mdxParser/mdxParserTypes.ts | 260 ++++++-------- components/src/mdxParser/mdxParsers.ts | 75 ++-- components/src/mdxParser/mdxValidators.ts | 344 ++++++++----------- components/src/page.tsx | 8 +- components/src/partials/events.tsx | 6 +- content/project-map.json | 80 ----- content/projects/bldrs.mdx | 2 +- 14 files changed, 376 insertions(+), 503 deletions(-) delete mode 100644 content/project-map.json diff --git a/app/(single-page)/[pageType]/[slug]/page.tsx b/app/(single-page)/[pageType]/[slug]/page.tsx index 89d483a..409b1b8 100644 --- a/app/(single-page)/[pageType]/[slug]/page.tsx +++ b/app/(single-page)/[pageType]/[slug]/page.tsx @@ -1,5 +1,7 @@ import { loadEvents, + loadFaqs, + loadPages, loadPosts, loadProjects, loadTrainings, @@ -7,7 +9,7 @@ import { import { Page } from "@opensource-construction/components"; import { notFound } from "next/navigation"; -type PageType = "events" | "projects" | "trainings" | "faqs" | "page"; +type PageType = "projects" | "events" | "trainings" | "page" | "post" | "faqs"; type SinglePageType = { pageType: PageType; @@ -16,30 +18,62 @@ type SinglePageType = { export async function generateStaticParams() { let posts: SinglePageType[] = []; + + // Ensure all loader functions are called synchronously + const projects = loadProjects(); + const events = loadEvents(); + const trainings = loadTrainings(); + const pages = loadPages(); + const faqs = loadFaqs(); + posts = [ ...posts, - ...loadProjects().map((p) => { - return { slug: p.slug, pageType: "projects" as PageType }; - }), - ...loadEvents().map((p) => { - return { slug: p.slug, pageType: "events" as PageType }; - }), - ...loadTrainings().map((p) => { - return { slug: p.slug, pageType: "trainings" as PageType }; - }), - ...loadPosts("page").map((p) => { - return { slug: p.slug, pageType: "page" as PageType }; - }), + ...projects.map((p) => ({ + slug: p.slug, + pageType: "projects" as PageType, + })), + ...events.map((e) => ({ slug: e.slug, pageType: "events" as PageType })), + ...trainings.map((t) => ({ + slug: t.slug, + pageType: "trainings" as PageType, + })), + ...pages.map((p) => ({ slug: p.slug, pageType: "page" as PageType })), + ...faqs.map((f) => ({ slug: f.slug, pageType: "faqs" as PageType })), ]; + return posts; } -export default function SinglePage({ params }: { params: SinglePageType }) { - let page = loadPosts(params.pageType).find( - (page) => page.slug === params.slug, - ); +export default async function SinglePage({ + params, +}: { + params: SinglePageType; +}) { + const { pageType, slug } = params; + + //TODO:FIX ANY + let page: any; - if (!page || params.pageType === "faqs") { + switch (pageType) { + case "projects": + page = loadProjects().find((p) => p.slug === slug); + break; + case "trainings": + page = loadTrainings().find((t) => t.slug === slug); + break; + case "events": + page = loadEvents().find((e) => e.slug === slug); + break; + case "faqs": + page = loadFaqs().find((f) => f.slug === slug); + break; + default: + page = loadPosts(pageType).find((p) => p.slug === slug); + } + + if (!page) { + notFound(); + } else if (pageType === "faqs") { notFound(); } diff --git a/app/actions.ts b/app/actions.ts index ecd1c24..f8dd887 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -25,7 +25,6 @@ export async function saveForm(previousState: Status, formData: FormData) { await document.loadInfo(); const sheet = document.sheetsById[+sheetId]; - console.log(formData); const schema = z.object({ firstname: z .string({ diff --git a/app/clusters/page.tsx b/app/clusters/page.tsx index 102f496..30e9bc3 100644 --- a/app/clusters/page.tsx +++ b/app/clusters/page.tsx @@ -1,5 +1,6 @@ import { getPosts, Section } from "@/components"; import { ClusterCard } from "@/components/src/clusterCard"; +import { loadPosts } from "@/components/src/mdxParser/mdxParsers"; import { ClustersPartial } from "@/components/src/partials/clusters"; export default function ClusterPage() { diff --git a/app/projects/page.tsx b/app/projects/page.tsx index 51c5cbc..5ee8cc9 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -2,8 +2,8 @@ import { Button, Section } from "@/components"; import { loadProjects } from "@/components/src/mdxParser/mdxParsers"; import { Maturity, - Project, - validMaturities, + OscProject, + VALID_MATURITIES, } from "@/components/src/mdxParser/mdxParserTypes"; function capitalizeFirstLetter(string: string): string { @@ -13,7 +13,7 @@ function capitalizeFirstLetter(string: string): string { export default function Projects() { let projects = loadProjects(); - const projectsByMaturity = projects.reduce>( + const projectsByMaturity = projects.reduce>( (acc, project) => { const maturity = project.metadata.maturity; if (!acc[maturity]) { @@ -22,13 +22,13 @@ export default function Projects() { acc[maturity].push(project); return acc; }, - {} as Record, + {} as Record, ); const sortedProjectsByMaturity = Object.entries(projectsByMaturity).sort( ([a], [b]) => - validMaturities.indexOf(a as Maturity) - - validMaturities.indexOf(b as Maturity), + VALID_MATURITIES.indexOf(a as Maturity) - + VALID_MATURITIES.indexOf(b as Maturity), ); //Set the color of the section based on the maturity of the projects diff --git a/app/trainings/page.tsx b/app/trainings/page.tsx index 97bedef..bca06f6 100644 --- a/app/trainings/page.tsx +++ b/app/trainings/page.tsx @@ -1,8 +1,8 @@ -import { getPosts, Section } from "@/components"; -import { loadTrainings } from "@opensource-construction/components/src/mdxParser/contentParser"; +import { Section } from "@/components"; +import { loadTrainings } from "@/components/src/mdxParser/mdxParsers"; +import { OscTraining } from "@/components/src/mdxParser/mdxParserTypes"; import { TrainingsPartial } from "@/components/src/partials/trainings"; import { TrainingCard } from "@/components/src/trainingCard"; -import { Training } from "@opensource-construction/components/src/mdxParser/parserTypes"; export default function Trainings() { let trainings = loadTrainings(); @@ -12,14 +12,14 @@ export default function Trainings() {
- {trainings.map((training: Training, index) => ( + {trainings.map((training: OscTraining, index) => ( ))}
diff --git a/components/__tests__/mdxValidators.test.ts b/components/__tests__/mdxValidators.test.ts index 134e623..14ae653 100644 --- a/components/__tests__/mdxValidators.test.ts +++ b/components/__tests__/mdxValidators.test.ts @@ -1,8 +1,8 @@ import { test, expect } from "vitest"; -import { Training } from "../src/mdxParser/mdxParserTypes"; +import { OscTraining } from "../src/mdxParser/mdxParserTypes"; import { validateTraining } from "../src/mdxParser/mdxValidators"; -const defaultTraining: Training = { +const defaultTraining: OscTraining = { type: "training", title: "Default Title", description: "Default Description", diff --git a/components/index.ts b/components/index.ts index d6e34c1..a538d48 100644 --- a/components/index.ts +++ b/components/index.ts @@ -5,11 +5,11 @@ import { Footer } from "./src/footer"; import { CustomMDX } from "./src/mdx"; import { Navbar } from "./src/nav"; import { Form } from "./src/form"; -import { Post } from "./src/mdxParser/mdxParserTypes"; +import { OscPost } from "./src/mdxParser/mdxParserTypes"; import { getPosts } from "./src/mdxParser/mdxParsers"; import { parseSlug } from "./src/mdxParser/mdxParsers"; -export type { Post }; +export type { OscPost as Post }; export { CustomMDX, Button, Section, Page, Footer, Navbar, Form }; export { getPosts, parseSlug }; diff --git a/components/src/mdxParser/mdxParserTypes.ts b/components/src/mdxParser/mdxParserTypes.ts index fbf89f6..81fcd4b 100644 --- a/components/src/mdxParser/mdxParserTypes.ts +++ b/components/src/mdxParser/mdxParserTypes.ts @@ -1,169 +1,140 @@ +// Constants as union types +export const VALID_MATURITIES = ["sandbox", "incubation", "graduated"] as const; +export const CONTENT_TYPES = ["project", "training", "post", "event", "page"] as const; +export const EVENT_STATUSES = ["TENTATIVE", "CONFIRMED", "CANCELLED"] as const; +export const TAG_CATEGORIES = ["type", "tool", "cost", "mode"] as const; + +// Type Definitions +export type ContentType = (typeof CONTENT_TYPES)[number]; +export type Maturity = (typeof VALID_MATURITIES)[number]; +export type EventStatus = (typeof EVENT_STATUSES)[number]; +export type TagCategory = (typeof TAG_CATEGORIES)[number]; +export type TrainingTag = `${TagCategory}::${string}`; -// Constants -export const validMaturities = ["sandbox", "incubation", "graduated"] as const; -export type ContentType = "project" | "training" | "post" | "event" | "page"; +// Common Types +export type ContentLink = { + url: string; + label: string; +}; -export type Content = Project | Training | Post | Event | Page; +type BaseMetadata = { + links: ContentLink[]; +}; +// Metadata Types +type ProjectMetadata = BaseMetadata & { + featured: boolean; + maturity: Maturity; +}; -// Base Types -interface BaseContent { +type TrainingMetadata = BaseMetadata & { + level: string; + duration: string; + author: string; + image: string; + tags: TrainingTag[]; +}; + +type EventMetadata = BaseMetadata & { + start: Date; + duration?: { + seconds?: number; + minutes?: number; + hours?: number; + days?: number; + weeks?: number; + }; + end?: Date; + location?: string; + geo?: { + lat: number; + lon: number; + }; + status: EventStatus; + organizer?: { + name: string; + email: string; + }; + url?: string; + isPast: boolean; +}; + +type PageMetadata = BaseMetadata & { + event?: Record; + project?: Record; +}; + +// Base Content Type +type BaseContent = { title: string; type: ContentType; -} - -interface WithDescription { description: string; -} - -interface WithSlug { slug: string; -} - -interface WithContent { content: string; -} - -interface WithLinks { - links: Array<{ url: string; label: string }>; -} +}; -// Project -export interface Project extends BaseContent, WithDescription, WithSlug, WithContent { +// Content Types +export type OscProject = BaseContent & { type: "project"; metadata: ProjectMetadata; -} - -interface ProjectMetadata extends WithLinks { - featured: boolean; - maturity: Maturity; -} - -export type Maturity = (typeof validMaturities)[number]; +}; -// Training -export interface Training extends BaseContent, WithDescription, WithSlug, WithContent { +export type OscTraining = BaseContent & { type: "training"; metadata: TrainingMetadata; -} - -interface TrainingMetadata extends WithLinks { - level?: string; - duration?: string; - author: string; - image: string; - tags: TrainingTag[]; -} +}; -// Event -export interface Event extends BaseContent, WithDescription, WithSlug, WithContent { +export type OscEvent = BaseContent & { type: "event"; metadata: EventMetadata; -} - -interface EventMetadata { - start: string; - duration?: Duration; - end?: string; - location?: string; - geo?: GeoLocation; - status?: EventStatus; - organizer?: Organizer; - url?: string; - isPast: boolean; - startDate: Date; -} - -type EventStatus = "TENTATIVE" | "CONFIRMED" | "CANCELLED"; - -interface GeoLocation { - lat: number; - lon: number; -} - -interface Organizer { - name: string; - email: string; -} - -interface Duration { - seconds?: number; - minutes?: number; - hours?: number; - days?: number; - weeks?: number; -} - -export type TagCategory = "type" | "tool" | "cost" | "mode"; -export type TrainingTag = `${TagCategory}::${string}`; +}; -export interface Page extends BaseContent, WithContent { +export type Page = BaseContent & { type: "page"; metadata: PageMetadata; -} - -interface PageMetadata extends WithLinks { - event?: object; - project?: object; -} - - -// metadata: { -// title: string; -// event?: object; -// project?: object; -// links?: { -// url: string; -// label: string; -// }[]; -// }; - -//TODO: for the time being use custom post -> Should be replaced with the below interface -export interface Post { +}; + +export type OscPost = { metadata: Record; slug: string; content: string; title?: string; description?: string; -} +}; -// export interface Post extends BaseContent, WithDescription, WithSlug, WithContent { -// type: "post"; -// metadata: Record; -// } +export type Content = OscProject | OscTraining | OscPost | OscEvent | Page; -type ContentDefaults = { - [K in ContentType]: DefaultContent; -}; +// Validator and Parser Types +export type ContentValidator = ( + raw: any, + slug: string, + content: string, + defaultContent: T +) => T; -type DefaultContent = T extends "project" - ? Omit & { type: "project" } - : T extends "training" - ? Omit & { type: "training" } - : T extends "event" - ? Omit & { type: "event" } - : T extends "page" - ? Omit & { type: "page" } - : T extends "post" - ? Omit & { type: "post" } - : never; - - -// Defaults -export const contentDefaults: ContentDefaults = { +export type Parser = ( + content: string, + slug: string, + metadata: unknown +) => T; + +// Default content with proper typing +export const contentDefaults: Record = { project: { - type: "project" as const, + type: "project", + title: "", description: "", slug: "", content: "", metadata: { featured: false, - maturity: "sandbox" as Maturity, + maturity: "sandbox", links: [], }, }, training: { - type: "training" as const, + type: "training", + title: "", description: "", slug: "", content: "", @@ -177,46 +148,35 @@ export const contentDefaults: ContentDefaults = { }, }, post: { - type: "post" as const, + title: "", description: "", slug: "", content: "", metadata: {}, }, event: { - type: "event" as const, + type: "event", + title: "", description: "", slug: "", content: "", metadata: { - status: "TENTATIVE" as EventStatus, - startDate: new Date(), - start: Intl.DateTimeFormat("en-UK", { - dateStyle: "short", - timeStyle: "short", - timeZone: "Europe/Zurich", - }).format(new Date()), + status: "TENTATIVE", + start: new Date(), isPast: false, + links: [], }, }, page: { - type: "page" as const, + type: "page", + title: "", + description: "", + slug: "", content: "", metadata: { links: [], event: {}, project: {}, }, - } -}; - -export type ContentValidator = ( - raw: any, - slug: string, - content: string, - defaultContent: T -) => T; -export interface Parser { - (content: string, slug: string, metadata: unknown): T; -} - + }, +} as const; \ No newline at end of file diff --git a/components/src/mdxParser/mdxParsers.ts b/components/src/mdxParser/mdxParsers.ts index 2ee2d29..9e00f1c 100644 --- a/components/src/mdxParser/mdxParsers.ts +++ b/components/src/mdxParser/mdxParsers.ts @@ -3,16 +3,25 @@ import fs from "fs"; import path from "path"; import YAML from "yaml"; import { - Content, - Project, - Training, + OscProject, + OscTraining, contentDefaults, ContentType, Parser, - Post, - Event + OscPost, + OscEvent, + Page, + ContentValidator } from "./mdxParserTypes"; -import { validateMetadata } from "./mdxValidators"; +import { contentValidators } from "./mdxValidators"; + +type ContentTypeMap = { + training: OscTraining; + event: OscEvent; + project: OscProject; + post: OscPost; + page: Page; +}; export function parseSlug(fileBasename: string) { let prefix = fileBasename.indexOf("-"); @@ -37,10 +46,10 @@ function parseFrontmatter(content: string) { } } -export function loadContent( +export function loadContent( dir: string, - type: ContentType, -): T[] { + type: T, +): ContentTypeMap[T][] { const contentDir = path.join(process.cwd(), "content", dir); try { @@ -51,18 +60,17 @@ export function loadContent( const content = fs.readFileSync(path.join(contentDir, file), "utf-8"); const slug = parseSlug(path.basename(file, ".mdx")); const parsed = parseFrontmatter(content); - - const defaultContent = contentDefaults[type] as T; + const validator = contentValidators[type] as ContentValidator; + const defaultContent = contentDefaults[type] as ContentTypeMap[T]; return parsed - ? validateMetadata( + ? validator( parsed.data, slug, parsed.body, - defaultContent, - type, + defaultContent ) - : { ...defaultContent, slug } as unknown as T; // Changed 'as T' to 'as unknown as T' + : { ...defaultContent, slug }; }); } catch (error) { console.error(`Error loading ${type}:`, error); @@ -70,7 +78,6 @@ export function loadContent( } } - export function getMDXFiles(dir: string): string[] { try { return fs @@ -90,15 +97,10 @@ export function readFile( validationFn: Parser, options = { encoding: "utf-8" as const }, ): T { - try { - const rawContent = fs.readFileSync(filePath, options); - const slug = parseSlug(path.basename(filePath, path.extname(filePath))); - const { metadata, content } = parseMdxFile(rawContent); - return validationFn(content, slug, metadata); - } catch (error) { - console.error("Error reading file:", error); - throw error; - } + const rawContent = fs.readFileSync(filePath, options); + const slug = parseSlug(path.basename(filePath, path.extname(filePath))); + const { metadata, content } = parseMdxFile(rawContent); + return validationFn(content, slug, metadata); } export function parseMdxFile(fileContent: string): { @@ -128,16 +130,15 @@ export function parseMdxFile(fileContent: string): { } } - /** * @deprecated Use specified `loaders` instead. */ -export function getPosts(dir?: string): Post[] { +export function getPosts(dir?: string): OscPost[] { const contentDir = path.join(process.cwd(), "content", dir || ""); try { const files = getMDXFiles(contentDir); return files.map((file) => - readFile( + readFile( path.join(contentDir, file), (content, slug, metadata) => ({ metadata: metadata as Record, @@ -152,19 +153,17 @@ export function getPosts(dir?: string): Post[] { } } -//Loaders +//LOADERS +export const loadProjects = () => loadContent("projects", "project"); -export const loadProjects = () => - loadContent("projects", "project"); +export const loadTrainings = () => loadContent("trainings", "training"); -export const loadTrainings = () => - loadContent("trainings", "training"); +export const loadEvents = () => loadContent("events", "event"); -export const loadEvents = (): Event[] => { - return loadContent("events", "event"); -}; +export const loadPages = () => loadContent("page", "page"); -export const loadPages = () => loadContent("page", "page") as Post[]; +export const loadPosts = (dir: string) => loadContent(dir, "post") as OscPost[]; -export const loadPosts = (dir: string) => loadContent(dir, "post") as Post[]; +//TODO: Implement this loader +export const loadFaqs = () => loadPosts("faqs"); diff --git a/components/src/mdxParser/mdxValidators.ts b/components/src/mdxParser/mdxValidators.ts index ca4c059..b738d35 100644 --- a/components/src/mdxParser/mdxValidators.ts +++ b/components/src/mdxParser/mdxValidators.ts @@ -1,242 +1,202 @@ import { - Training, - Event, - Project, + OscTraining, + OscEvent, + OscProject, ContentType, Maturity, - validMaturities, - Content, + VALID_MATURITIES, TrainingTag, Page, + EventStatus, + EVENT_STATUSES, + ContentLink, + OscPost, } from "./mdxParserTypes"; import { ContentValidator } from "./mdxParserTypes"; -export function validatePostMetadata(metadata: any) { - return metadata || {}; -} +type ContentTypeMap = { + training: OscTraining; + event: OscEvent; + project: OscProject; + post: OscPost; + page: Page; +}; -export const validators = { - project: validateProjectMetadata, - training: validateTrainingMetadata, - post: validatePostMetadata, - event: validateEventMetadata, - page: validatePageMetadata, +type ContentValidatorMap = { + [K in ContentType]: ContentValidator; }; -/** - * Validates and transforms raw training content into a structured format. - * - * @param raw - The raw training content to validate. - * @param slug - The slug identifier for the training content. - * @param content - The main content of the training. - * @param defaultContent - The default content to fall back on if raw content is missing. - * @returns The validated and structured training content. - */ -export const validateTraining: ContentValidator = ( - raw, - slug, - content, - defaultContent, -) => ({ - ...defaultContent, - title: raw?.title || defaultContent.title, - description: raw?.description || defaultContent.description, - slug, - content, - metadata: { - ...defaultContent.metadata, - level: raw?.metadata?.level || "beginner", - duration: raw?.metadata?.duration || "1h", - author: raw?.author || "", - image: raw?.image || "", - links: raw?.links || [], - tags: Array.isArray(raw?.tags) ? raw.tags : [], - }, -}); +function ensureString(value: any, defaultValue: string = ""): string { + if (typeof value === 'string') return value; + return String(defaultValue); +} -export function validateTrainingMetadata(metadata: any) { - return metadata || {}; +function ensureBoolean(value: any): boolean { + return Boolean(value); } -/** - * Validates and processes raw event data into a structured Event object. - * - * @param raw - The raw event data to validate and process. - * @param slug - The slug identifier for the event. - * @param content - The content of the event. - * @param defaultContent - The default content to use if certain fields are missing in the raw data. - * @returns A validated and structured Event object. - */ -export const validateEvent: ContentValidator = ( - raw, - slug, - content, - defaultContent, -) => { - const eventStart = new Date(raw?.event?.start || defaultContent.metadata.start); +function ensureDate(date: any): Date { + if (date instanceof Date && !isNaN(date.getTime())) return date; + if (typeof date === 'string') { + const parsed = new Date(date); + if (!isNaN(parsed.getTime())) return parsed; + } + return new Date(); +} + +function ensureLinks(links: any): ContentLink[] { + if (!Array.isArray(links)) return []; + return links.filter(link => + typeof link === 'object' && + typeof link?.url === 'string' && + typeof link?.label === 'string' + ); +} + +function ensureTrainingTags(tags: any): TrainingTag[] { + if (!Array.isArray(tags)) return []; + return tags.filter(isValidTrainingTag); +} + +function isValidTrainingTag(tag: string): tag is TrainingTag { + return /^(type|tool|cost|mode)::.+$/.test(tag); +} + +function parseMaturity(maturity: any): Maturity { + const cleanedMaturity = String(maturity || "") + .trim() + .toLowerCase(); + return VALID_MATURITIES.includes(cleanedMaturity as Maturity) + ? (cleanedMaturity as Maturity) + : "sandbox"; +} + +function ensureEventStatus(status: any): EventStatus { + return EVENT_STATUSES.includes(status as EventStatus) + ? status as EventStatus + : "TENTATIVE"; +} + +// Content Validators +export const validateEvent: ContentValidator = ( + raw: any, + slug: string, + content: string, + defaultContent: OscEvent, +): OscEvent => { + const eventStart = ensureDate(raw?.event?.start || defaultContent.metadata.start); const isPast = eventStart.getTime() < Date.now(); - const formattedStart = Intl.DateTimeFormat("en-UK", { - dateStyle: "short", - timeStyle: "short", - timeZone: "Europe/Zurich", - }).format(eventStart); return { ...defaultContent, type: "event", - title: raw?.title || defaultContent.title, - description: raw?.description || defaultContent.description, + title: ensureString(raw?.title, defaultContent.title), + description: ensureString(raw?.description, defaultContent.description), slug, content, metadata: { ...defaultContent.metadata, + start: eventStart, duration: raw?.event?.duration || { hours: 1 }, - location: raw?.event?.location, - geo: raw?.event?.geo, - status: raw?.event?.status || "TENTATIVE", - organizer: raw?.event?.organizer, - url: raw?.event?.url, - start: formattedStart, - isPast: isPast, + location: ensureString(raw?.event?.location), + geo: raw?.event?.geo ? { + lat: Number(raw.event.geo.lat) || 0, + lon: Number(raw.event.geo.lon) || 0, + } : undefined, + status: ensureEventStatus(raw?.event?.status), + organizer: raw?.event?.organizer ? { + name: ensureString(raw.event.organizer.name), + email: ensureString(raw.event.organizer.email), + } : undefined, + url: ensureString(raw?.event?.url), + isPast, + links: ensureLinks(raw?.links), }, }; }; -export function validateEventMetadata(metadata: any) { - return { - duration: metadata?.duration, - end: metadata?.end, - location: metadata?.location, - geo: metadata?.geo, - status: metadata?.status || "TENTATIVE", - organizer: metadata?.organizer, - url: metadata?.url, - isPast: new Date(metadata?.start).getTime() < Date.now(), - startDate: metadata?.start ? new Date(metadata.start) : new Date(), - start: metadata?.start - ? Intl.DateTimeFormat("en-UK", { - dateStyle: "short", - timeStyle: "short", - timeZone: "Europe/Zurich", - }).format(new Date(metadata.start)) - : Intl.DateTimeFormat("en-UK", { - dateStyle: "short", - timeStyle: "short", - timeZone: "Europe/Zurich", - }).format(new Date()), - }; -} - -/** - * Validates and constructs a Project object by merging raw input data with default content. - * - * @param raw - The raw project data that needs to be validated and merged. - * @param slug - The unique identifier for the project. - * @param content - The main content of the project. - * @param defaultContent - The default content to fall back on if raw data is incomplete. - * @returns A validated and merged Project object. - */ -const validateProject: ContentValidator = ( - raw, +export const validateProject: ContentValidator = ( + raw: any, + slug: string, + content: string, + defaultContent: OscProject, +): OscProject => ({ + ...defaultContent, + type: "project", + title: ensureString(raw?.title, defaultContent.title), + description: ensureString(raw?.description, defaultContent.description), slug, content, - defaultContent, -) => ({ + metadata: { + ...defaultContent.metadata, + links: ensureLinks(raw?.links), + featured: ensureBoolean(raw?.metadata?.featured), + maturity: parseMaturity(raw?.metadata?.maturity), + }, +}); + +export const validateTraining: ContentValidator = ( + raw: any, + slug: string, + content: string, + defaultContent: OscTraining, +): OscTraining => ({ ...defaultContent, - title: raw?.title || defaultContent.title, - description: raw?.description || defaultContent.description, + type: "training", + title: ensureString(raw?.title, defaultContent.title), + description: ensureString(raw?.description, defaultContent.description), slug, content, - links: raw?.links || [], metadata: { ...defaultContent.metadata, - featured: raw?.metadata?.featured || false, - maturity: raw?.metadata?.maturity || "sandbox", + level: ensureString(raw?.metadata?.level, "beginner"), + duration: ensureString(raw?.metadata?.duration, ""), + author: ensureString(raw?.author), + image: ensureString(raw?.image), + links: ensureLinks(raw?.links), + tags: ensureTrainingTags(raw?.tags), }, }); -export function validateProjectMetadata(metadata: any) { - return { - featured: !!metadata?.featured, - maturity: parseMaturity(metadata?.maturity), - }; -} - -function parseMaturity(maturity: any): Maturity { - const cleanedMaturity = String(maturity || "") - .trim() - .toLowerCase(); - return validMaturities.includes(cleanedMaturity as Maturity) - ? (cleanedMaturity as Maturity) - : "sandbox"; -} - -const validatePage: ContentValidator = ( - raw, +export const validatePost: ContentValidator = ( + raw: any, + slug: string, + content: string, + defaultContent: OscPost, +): OscPost => ({ + title: ensureString(raw?.title), + description: ensureString(raw?.description), slug, content, - defaultContent, -) => ({ + metadata: raw?.metadata || {}, +}); + +export const validatePage: ContentValidator = ( + raw: any, + slug: string, + content: string, + defaultContent: Page, +): Page => ({ ...defaultContent, - title: raw?.title || defaultContent.title, - content, - metadata: validatePageMetadata(raw?.metadata), + type: "page", + title: ensureString(raw?.title, defaultContent.title), + description: ensureString(raw?.description, defaultContent.description), slug, - + content, + metadata: { + ...defaultContent.metadata, + links: ensureLinks(raw?.links), + event: raw?.metadata?.event || {}, + project: raw?.metadata?.project || {}, + }, }); -function validatePageMetadata(metadata: any) { - return { - ...metadata, - links: metadata?.links || [], - events: metadata?.events || [], - projects: metadata?.projects || [], - } -} - -/** - * A record of content validators for different content types. - * Each validator function takes raw content, a slug, processed content, - * and default content, and returns the validated content. - */ -export const contentValidators: Record> = { +// Export contentValidators with proper typing +export const contentValidators: ContentValidatorMap = { training: validateTraining, event: validateEvent, project: validateProject, - post: (raw, slug, content, defaultContent) => ({ - ...defaultContent, - title: raw?.title || "", - description: raw?.description || "", - slug, - content, - metadata: raw?.metadata || {}, - }), + post: validatePost, page: validatePage, }; -/** - * Validates the metadata of the given content. - * - * @template T - The type of the content. - * @param raw - The raw metadata to be validated. - * @param slug - The slug of the content. - * @param content - The content to be validated. - * @param defaultContent - The default content to fall back on if validation fails. - * @param type - The type of the content. - * @returns The validated content. - */ -export function validateMetadata( - raw: any, - slug: string, - content: string, - defaultContent: T, - type: ContentType, -): T { - const validator = contentValidators[type]; - return validator(raw, slug, content, defaultContent); -} - -export function isValidTrainingTag(tag: string): tag is TrainingTag { - return /^(type|tool|cost|mode)::.+$/.test(tag); -} - diff --git a/components/src/page.tsx b/components/src/page.tsx index 22afe58..1e05322 100644 --- a/components/src/page.tsx +++ b/components/src/page.tsx @@ -7,10 +7,10 @@ export default function Page({ page, }: { page: { + title: string; + event?: object; + project?: object; metadata: { - title: string; - event?: object; - project?: object; links?: { url: string; label: string; @@ -30,7 +30,7 @@ export default function Page({ icon="left" />

- {page.metadata.title} + {page.title}

diff --git a/components/src/partials/events.tsx b/components/src/partials/events.tsx index 99dd811..95bd222 100644 --- a/components/src/partials/events.tsx +++ b/components/src/partials/events.tsx @@ -5,7 +5,7 @@ export function EventsPartial({ showPast = false }: { showPast?: boolean }) { let eventsParsed = loadEvents(); let es = eventsParsed - .sort((a, b) => (a.metadata.startDate < b.metadata.startDate ? 1 : -1)) + .sort((a, b) => (a.metadata.start < b.metadata.start ? 1 : -1)) .filter( (e) => (e.metadata.isPast && showPast) || (!e.metadata.isPast && !showPast), @@ -17,11 +17,11 @@ export function EventsPartial({ showPast = false }: { showPast?: boolean }) { ? "No pending events" : es.map((e) => ( ))} diff --git a/content/project-map.json b/content/project-map.json deleted file mode 100644 index 921528c..0000000 --- a/content/project-map.json +++ /dev/null @@ -1,80 +0,0 @@ -[ - { - "title": "Bonsai", - "slug": "bonsai", - "featured": true, - "maturity": "graduated" - }, - { - "title": "Compas", - "slug": "compas", - "featured": true, - "maturity": "graduated" - }, - { - "title": "IFC Model Checker", - "slug": "ifc-model-checker", - "featured": true, - "maturity": "sandbox" - }, - { - "title": "PyRevit", - "slug": "pyrevit", - "featured": false, - "maturity": "graduated" - }, - { - "title": "Sprint", - "slug": "sprint", - "featured": false, - "maturity": "incubation" - }, - { - "title": "Vyssuals", - "slug": "vyssuals", - "featured": false, - "maturity": "incubation" - }, - { - "title": "Circular Construction Co-Pilot", - "slug": "circular-construction-co-pilot", - "featured": false, - "maturity": "sandbox" - }, - { - "title": "IFC LCA", - "slug": "ifc-lca", - "featured": false, - "maturity": "incubation" - }, - { - "title": "LCAX and EPDX", - "slug": "lcax-and-epdx", - "featured": false, - "maturity": "sandbox" - }, - { - "title": "Speckle", - "slug": "speckle", - "featured": false, - "maturity": "graduated" - }, - { - "title": "That Open Company", - "slug": "that-open-company", - "featured": false, - "maturity": "graduated" - }, - { - "title": "Calc", - "slug": "calc", - "featured": false, - "maturity": "incubation" - }, - { - "title": "Share by bldrs.ai", - "slug": "bldrs", - "featured": false, - "maturity": "sandbox" - } -] diff --git a/content/projects/bldrs.mdx b/content/projects/bldrs.mdx index 6b51da3..f40df10 100644 --- a/content/projects/bldrs.mdx +++ b/content/projects/bldrs.mdx @@ -3,7 +3,7 @@ title: Share by bldrs.ai description: Bldrs specializes in developing web-based CAD collaboration tools designed to meet the needs of modern engineering workflows providing real-time collaboration and visualization capabilities for architects, engineers, and designers. metadata: featured: true - maturity: incubation + maturity: sandbox links: - url: http://bldrs.ai label: Website From 1357c499be8db19ef4dfd7734c8ea4e030424f9e Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Fri, 13 Dec 2024 10:54:17 +0100 Subject: [PATCH 08/33] refactor: replace processPosts with getPosts in FAQPartial component --- components/src/partials/faq.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/src/partials/faq.tsx b/components/src/partials/faq.tsx index 742adc3..b27f74a 100644 --- a/components/src/partials/faq.tsx +++ b/components/src/partials/faq.tsx @@ -1,10 +1,10 @@ import { CustomMDX } from "../mdx"; -import { processPosts } from "../utils"; import { Card } from "../card"; +import { getPosts } from "../mdxParser/mdxParsers"; export function FAQPartial() { - const faqs = processPosts("faqs"); + const faqs = getPosts("faqs"); return (
From 82646cd9a9653fc8618a6badd3e775edcbceda17 Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:01:12 +0100 Subject: [PATCH 09/33] refactor: remove zod dependency from package.json --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 1c4b2e6..07fc4da 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,7 @@ "react-dom": "^18", "react-icons": "^5.2.1", "slugify": "^1.6.6", - "yaml": "^2.4.2", - "zod": "^3.24.0" + "yaml": "^2.4.2" }, "devDependencies": { "@chromatic-com/storybook": "^1.5.0", From 0fe5b1bf44357639c815468e8afd16000e7b6f1b Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Fri, 13 Dec 2024 16:20:18 +0100 Subject: [PATCH 10/33] refactor: implement dynamic page rendering and enhance cluster support in mdxParser --- app/(single-page)/[pageType]/[slug]/page.tsx | 8 ++- app/clusters/page.tsx | 24 +++---- app/projects/page.tsx | 3 - components/src/card/card.tsx | 2 +- components/src/dynamicPage.tsx | 70 ++++++++++++++++++++ components/src/mdxParser/mdxParserTypes.ts | 41 +++++++++++- components/src/mdxParser/mdxParsers.ts | 18 ++--- components/src/mdxParser/mdxValidators.ts | 38 +++++++++-- components/src/page.tsx | 4 +- package.json | 3 +- 10 files changed, 165 insertions(+), 46 deletions(-) create mode 100644 components/src/dynamicPage.tsx diff --git a/app/(single-page)/[pageType]/[slug]/page.tsx b/app/(single-page)/[pageType]/[slug]/page.tsx index 409b1b8..0fb72b2 100644 --- a/app/(single-page)/[pageType]/[slug]/page.tsx +++ b/app/(single-page)/[pageType]/[slug]/page.tsx @@ -1,3 +1,4 @@ +import DynamicPage from "@/components/src/dynamicPage"; import { loadEvents, loadFaqs, @@ -6,6 +7,7 @@ import { loadProjects, loadTrainings, } from "@/components/src/mdxParser/mdxParsers"; +import { Content } from "@/components/src/mdxParser/mdxParserTypes"; import { Page } from "@opensource-construction/components"; import { notFound } from "next/navigation"; @@ -52,7 +54,7 @@ export default async function SinglePage({ const { pageType, slug } = params; //TODO:FIX ANY - let page: any; + let page: Content | undefined; switch (pageType) { case "projects": @@ -68,7 +70,7 @@ export default async function SinglePage({ page = loadFaqs().find((f) => f.slug === slug); break; default: - page = loadPosts(pageType).find((p) => p.slug === slug); + // page = loadPosts(pageType).find((p) => p.slug === slug); } if (!page) { @@ -77,5 +79,5 @@ export default async function SinglePage({ notFound(); } - return ; + return ; } diff --git a/app/clusters/page.tsx b/app/clusters/page.tsx index 30e9bc3..a232ec4 100644 --- a/app/clusters/page.tsx +++ b/app/clusters/page.tsx @@ -1,27 +1,25 @@ import { getPosts, Section } from "@/components"; import { ClusterCard } from "@/components/src/clusterCard"; -import { loadPosts } from "@/components/src/mdxParser/mdxParsers"; +import { loadClusters } from "@/components/src/mdxParser/mdxParsers"; import { ClustersPartial } from "@/components/src/partials/clusters"; export default function ClusterPage() { - let clusters = getPosts("clusters"); + let clusters = loadClusters(); return (
- {clusters.map( - ({ slug, metadata: { title, description, image } }, index) => ( - - ), - )} + {clusters.map((c, index) => ( + + ))}
); diff --git a/app/projects/page.tsx b/app/projects/page.tsx index 5ee8cc9..d899f03 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -46,9 +46,6 @@ export default function Projects() { "Projects considered stable, widely adopted and production ready, attracting hundreds of users and contributors", }; - //TODO: Improve the layout of this page especially regarding visual hierarchy - //TODO: Either create a special card for projects in the project dir or use a different layout - return (
diff --git a/components/src/card/card.tsx b/components/src/card/card.tsx index 6018164..1ad62ed 100644 --- a/components/src/card/card.tsx +++ b/components/src/card/card.tsx @@ -49,7 +49,7 @@ export function Card({
+ {Array.isArray(page.metadata.links) && page.metadata.links.length > 0 && ( +
+

Links

+
+ {page.metadata.links.map((l) => ( +
+
+ )} +
+ ); +} diff --git a/components/src/mdxParser/mdxParserTypes.ts b/components/src/mdxParser/mdxParserTypes.ts index 81fcd4b..b762fd1 100644 --- a/components/src/mdxParser/mdxParserTypes.ts +++ b/components/src/mdxParser/mdxParserTypes.ts @@ -1,6 +1,6 @@ // Constants as union types export const VALID_MATURITIES = ["sandbox", "incubation", "graduated"] as const; -export const CONTENT_TYPES = ["project", "training", "post", "event", "page"] as const; +export const CONTENT_TYPES = ["project", "training", "post", "event", "page", "cluster"] as const; export const EVENT_STATUSES = ["TENTATIVE", "CONFIRMED", "CANCELLED"] as const; export const TAG_CATEGORIES = ["type", "tool", "cost", "mode"] as const; @@ -89,12 +89,33 @@ export type OscEvent = BaseContent & { metadata: EventMetadata; }; -export type Page = BaseContent & { +export type OscPage = BaseContent & { type: "page"; metadata: PageMetadata; }; +export type OscCluster = BaseContent & { + type: "cluster"; + metadata: OscClusterMetadata; +} + +export type OscClusterMetadata = { + image?: string; + links?: ContentLink[]; + partners?: OscClusterPartner[]; + tags?: string[]; +} + + +export type OscClusterPartner = { + url: string; + name: string; + log: string; // URL to logo +} + + export type OscPost = { + type: "post"; metadata: Record; slug: string; content: string; @@ -102,7 +123,7 @@ export type OscPost = { description?: string; }; -export type Content = OscProject | OscTraining | OscPost | OscEvent | Page; +export type Content = OscProject | OscTraining | OscPost | OscEvent | OscPage | OscCluster; // Validator and Parser Types export type ContentValidator = ( @@ -148,6 +169,7 @@ export const contentDefaults: Record = { }, }, post: { + type: "post", title: "", description: "", slug: "", @@ -179,4 +201,17 @@ export const contentDefaults: Record = { project: {}, }, }, + cluster: { + type: "cluster", + title: "", + description: "", + slug: "", + content: "", + metadata: { + image: "", + links: [], + partners: [], + tags: [], + }, + }, } as const; \ No newline at end of file diff --git a/components/src/mdxParser/mdxParsers.ts b/components/src/mdxParser/mdxParsers.ts index 9e00f1c..17964f7 100644 --- a/components/src/mdxParser/mdxParsers.ts +++ b/components/src/mdxParser/mdxParsers.ts @@ -3,25 +3,13 @@ import fs from "fs"; import path from "path"; import YAML from "yaml"; import { - OscProject, - OscTraining, contentDefaults, ContentType, Parser, OscPost, - OscEvent, - Page, - ContentValidator + ContentValidator, } from "./mdxParserTypes"; -import { contentValidators } from "./mdxValidators"; - -type ContentTypeMap = { - training: OscTraining; - event: OscEvent; - project: OscProject; - post: OscPost; - page: Page; -}; +import { ContentTypeMap, contentValidators } from "./mdxValidators"; export function parseSlug(fileBasename: string) { let prefix = fileBasename.indexOf("-"); @@ -162,6 +150,8 @@ export const loadEvents = () => loadContent("events", "event"); export const loadPages = () => loadContent("page", "page"); +export const loadClusters = () => loadContent("clusters", "cluster"); + export const loadPosts = (dir: string) => loadContent(dir, "post") as OscPost[]; //TODO: Implement this loader diff --git a/components/src/mdxParser/mdxValidators.ts b/components/src/mdxParser/mdxValidators.ts index b738d35..756c18d 100644 --- a/components/src/mdxParser/mdxValidators.ts +++ b/components/src/mdxParser/mdxValidators.ts @@ -6,20 +6,22 @@ import { Maturity, VALID_MATURITIES, TrainingTag, - Page, + OscPage, EventStatus, EVENT_STATUSES, ContentLink, OscPost, + OscCluster, } from "./mdxParserTypes"; import { ContentValidator } from "./mdxParserTypes"; -type ContentTypeMap = { +export type ContentTypeMap = { training: OscTraining; event: OscEvent; project: OscProject; post: OscPost; - page: Page; + page: OscPage; + cluster: OscCluster; }; type ContentValidatorMap = { @@ -164,6 +166,7 @@ export const validatePost: ContentValidator = ( content: string, defaultContent: OscPost, ): OscPost => ({ + ...defaultContent, title: ensureString(raw?.title), description: ensureString(raw?.description), slug, @@ -171,12 +174,12 @@ export const validatePost: ContentValidator = ( metadata: raw?.metadata || {}, }); -export const validatePage: ContentValidator = ( +export const validatePage: ContentValidator = ( raw: any, slug: string, content: string, - defaultContent: Page, -): Page => ({ + defaultContent: OscPage, +): OscPage => ({ ...defaultContent, type: "page", title: ensureString(raw?.title, defaultContent.title), @@ -191,12 +194,33 @@ export const validatePage: ContentValidator = ( }, }); -// Export contentValidators with proper typing +export const validateCluster: ContentValidator = ( + raw: any, + slug: string, + content: string, + defaultContent: OscCluster, +): OscCluster => ({ + ...defaultContent, + type: "cluster", + title: ensureString(raw?.title, defaultContent.title), + description: ensureString(raw?.description, defaultContent.description), + slug, + content, + metadata: { + ...defaultContent.metadata, + links: ensureLinks(raw?.links), + image: ensureString(raw?.image), + partners: raw?.partners || [], + tags: raw?.tags || [], + }, +}); + export const contentValidators: ContentValidatorMap = { training: validateTraining, event: validateEvent, project: validateProject, post: validatePost, page: validatePage, + cluster: validateCluster, }; diff --git a/components/src/page.tsx b/components/src/page.tsx index 1e05322..400e009 100644 --- a/components/src/page.tsx +++ b/components/src/page.tsx @@ -2,6 +2,7 @@ import { CustomMDX } from "./mdx"; import { Button } from "./button"; import { Section } from "./section"; import React from "react"; +import { Content } from "./mdxParser/mdxParserTypes"; export default function Page({ page, @@ -10,6 +11,7 @@ export default function Page({ title: string; event?: object; project?: object; + type: string; metadata: { links?: { url: string; @@ -24,7 +26,7 @@ export default function Page({
+
+ +
+ +
+
+ +
+ + ); +} diff --git a/app/globals.css b/app/globals.css index 5160eb1..0651699 100644 --- a/app/globals.css +++ b/app/globals.css @@ -14,3 +14,13 @@ svg:not(:root) { overflow-clip-margin: content-box; overflow: hidden; } + +.transition-all { + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 300ms; +} + +.section-transition { + transition: background-color 500ms ease-in-out; +} diff --git a/app/layout.tsx b/app/layout.tsx index 6a994ea..f369d42 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -33,7 +33,7 @@ export const metadata: Metadata = { const navItems = [ { name: "Projects", target: "/projects" }, - { name: "Events", target: "/#events" }, + { name: "Events", target: "/events" }, { name: "Trainings", target: "/trainings" }, { name: "About us", target: "/#who-is-behind-the-initiative" }, ]; diff --git a/app/page.tsx b/app/page.tsx index fe8cab1..124ae03 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -13,6 +13,7 @@ import partners from "../content/partners.json"; import ContactCard from "@/components/src/contactCard"; import PartnerCard from "@/components/src/partnerCard"; +import { EventsIndexPartial } from "@/components/src/partials/events"; export default function Home() { const discordLink = process.env.DISCORD_LINK || ""; @@ -77,11 +78,11 @@ export default function Home() { exchange – find them here + further events that we attend as well.

- - -
- +
+ {/*
+ +
*/}
{ + return date.getTime() < Date.now(); +}; + export function EventsPartial({ showPast = false }: { showPast?: boolean }) { let eventsParsed = loadEvents(); @@ -28,3 +33,81 @@ export function EventsPartial({ showPast = false }: { showPast?: boolean }) { ); } + +export function EventsIndexPartial() { + let eventsParsed = loadEvents(); + + const pastEvents = eventsParsed + .filter((e) => isPastEvent(e.metadata.start)) + .sort((a, b) => (a.metadata.start < b.metadata.start ? 1 : -1)) + .slice(0, 3); + + const futureEvents = eventsParsed + .filter((e) => !isPastEvent(e.metadata.start)) + .sort((a, b) => (a.metadata.start < b.metadata.start ? 1 : -1)); + + return ( +
+ {/* Upcoming Events Section */} +
+

+ Upcoming Events +

+
+ {futureEvents.length === 0 ? ( +

No upcoming events

+ ) : ( + futureEvents.map((e) => ( + + )) + )} +
+
+ + {/* Past Events Section */} + {pastEvents.length > 0 && ( +
+

+ Past Events +

+
+ {pastEvents.map((e) => ( +
+ +
+ ))} +
+

+ Discover More +

+

+ View our complete archive of past and upcoming events +

+
+
+
+
+
+ )} +
+ ); +} diff --git a/content/partners/network-partners.json b/content/partners/network-partners.json new file mode 100644 index 0000000..97e97a8 --- /dev/null +++ b/content/partners/network-partners.json @@ -0,0 +1,79 @@ +[ + { + "name": "metaXD", + "logo": "metaxd.png", + "url": "https://www.metaxd.ch/", + "type": "industry", + "tier": "network_partner" + }, + { + "name": "2050 materials", + "logo": "2050materials.png", + "url": "https://2050-materials.com/", + "type": "industry", + "tier": "network_partner" + }, + { + "name": "Pirmin Jung", + "logo": "pirminjung.png", + "url": "https://www.pirminjung.ch/", + "type": "industry", + "tier": "network_partner" + }, + { + "name": "Balteschwiler", + "logo": "balteschwiler.png", + "url": "https://balteschwiler.ch/", + "type": "industry", + "tier": "network_partner" + }, + { + "name": "Borm", + "logo": "borm.png", + "url": "https://www.borm.com/", + "type": "industry", + "tier": "network_partner" + }, + { + "name": "lignocam", + "logo": "lignocam.jpg", + "url": "https://lignocam.com/", + "type": "industry", + "tier": "network_partner" + }, + { + "name": "Schär Holzbau", + "logo": "schaer.png", + "url": "https://schaerholzbau.ch/", + "type": "industry", + "tier": "network_partner" + }, + { + "name": "DesignToProduction", + "logo": "dtp.png", + "url": "https://www.designtoproduction.com/", + "type": "industry", + "tier": "network_partner" + }, + { + "name": "Cadwork", + "logo": "cadwork.png", + "url": "https://www.cadwork.com/", + "type": "industry", + "tier": "network_partner" + }, + { + "name": "Gumpp & Maier", + "logo": "gumppmaier.png", + "url": "https://www.gumpp-maier.de/", + "type": "industry", + "tier": "network_partner" + }, + { + "name": "Penzel Valier", + "logo": "penzelvalier.png", + "url": "https://www.penzelvalier.ch/", + "type": "industry", + "tier": "network_partner" + } +] From a08766f34440778b90f4ee6b11d4dd14b8971d2f Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:34:38 +0100 Subject: [PATCH 21/33] remove redundant code --- app/events/page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/events/page.tsx b/app/events/page.tsx index 43dbb6e..0548e6c 100644 --- a/app/events/page.tsx +++ b/app/events/page.tsx @@ -2,8 +2,6 @@ import { Button, EventsPartial, Section } from "@/components"; import { loadEvents } from "@/lib/mdxParser/mdxParser"; export default function Events({}: {}) { - const events = loadEvents(); - return (
From 3b97dd1569f8138f5ff56365670b09de4c7a241e Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:11:53 +0100 Subject: [PATCH 22/33] Fix pull issue with project map --- content/project-map.json | 86 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 content/project-map.json diff --git a/content/project-map.json b/content/project-map.json new file mode 100644 index 0000000..94d8dad --- /dev/null +++ b/content/project-map.json @@ -0,0 +1,86 @@ +[ + { + "title": "Bonsai", + "slug": "bonsai", + "featured": true, + "maturity": "graduated" + }, + { + "title": "Compas", + "slug": "compas", + "featured": true, + "maturity": "graduated" + }, + { + "title": "IFC Model Checker", + "slug": "ifc-model-checker", + "featured": true, + "maturity": "sandbox" + }, + { + "title": "PyRevit", + "slug": "pyrevit", + "featured": false, + "maturity": "graduated" + }, + { + "title": "Sprint", + "slug": "sprint", + "featured": false, + "maturity": "incubation" + }, + { + "title": "Vyssuals", + "slug": "vyssuals", + "featured": false, + "maturity": "incubation" + }, + { + "title": "Circular Construction Co-Pilot", + "slug": "circular-construction-co-pilot", + "featured": false, + "maturity": "sandbox" + }, + { + "title": "IFC LCA", + "slug": "ifc-lca", + "featured": false, + "maturity": "incubation" + }, + { + "title": "LCAX and EPDX", + "slug": "lcax-and-epdx", + "featured": false, + "maturity": "sandbox" + }, + { + "title": "Speckle", + "slug": "speckle", + "featured": false, + "maturity": "graduated" + }, + { + "title": "That Open Company", + "slug": "that-open-company", + "featured": false, + "maturity": "graduated" + }, + { + "title": "Calc", + "slug": "calc", + "featured": false, + "maturity": "incubation" + }, + { + "title": "Share by bldrs.ai", + "slug": "bldrs", + "featured": false, + "maturity": "sandbox" + }, + { + "title": "BetterCorrectFast", + "slug": "BetterCorrectFast", + "featured": false, + "maturity": "sandbox" + } +] \ No newline at end of file From d5bf1549927530185d1c6d48ee523ed1f6eef415 Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:27:48 +0100 Subject: [PATCH 23/33] merging from main --- .../events/20250202-aec-hackathon-zurich-2025.mdx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/content/events/20250202-aec-hackathon-zurich-2025.mdx b/content/events/20250202-aec-hackathon-zurich-2025.mdx index 923848c..3ef29ca 100644 --- a/content/events/20250202-aec-hackathon-zurich-2025.mdx +++ b/content/events/20250202-aec-hackathon-zurich-2025.mdx @@ -12,7 +12,7 @@ links: ![AEC Hackathon - Zurich Edition](/images/events/20250207_AEC-Hackathon-ZurichV7.png "AEC Hackathon - Zurich Edition") -Our first hackathon in Zurich was a huge success with over 250 hackers and visitors. We are super happy that we managed to bring the same energy and innovation to our recent Hackathon in Munich, organised together with the TUM Venture Lab. +Our first hackathon in Zurich was a huge success with over 250 hackers and visitors. We are super happy that we managed to bring the same energy and innovation to our recent Hackathon in Munich, organised together with the TUM Venture Lab. And now it's time to roll up our sleeves again and get to work in Zurich, where this year’s AEC Hachathon will again be hosted by the [ZHAW Institute of Building Technology and Process](https://www.zhaw.ch/de/archbau)! This time, the [Institute for Digital Construction and Wood Industry](https://www.bfh.ch/en/research/research-areas/institute-digital-construction-wood-industry/) from Berner Fachhochschule, led by Katharina Lindenberg, will join us as Key Partner for the Friday session. In this session, we will investigate the potential for code-based collaboration in the wood industry. Can this kind of collaboration help to secure the technological advantages of the swiss timber industry? Come by and find out with us! @@ -40,11 +40,11 @@ Whether you're a seasoned professional or a passionate student, this hackathon i ![AEC Hackathon - Zurich Edition focus session](/images/events/ZRH25-Focus-Session-Wood.png "AEC Hackathon - Zurich Edition focus session") -The Hackathon starts on Friday evening. The Hackathon itself is not entirely related to the afternoon's symposium. The beauty of the event is that a variety of different people get together and get to hack with others from outside their organisation or "bubble". +The Hackathon starts on Friday evening. The Hackathon itself is not entirely related to the afternoon's symposium. The beauty of the event is that a variety of different people get together and get to hack with others from outside their organisation or "bubble". There will be five sponsored challenges from our partners. In addition, participants have the possibility to announce their own challenge spontaneously as well. If you are considering to bring your own challenge, please get in touch with Max at [max@opensource.construction](mailto:max@opensource.construction). -- [Challenge 1](https://drive.google.com/file/d/15ywkJPAj2DExlBq-6XFlsraUkESm8Ffw/view?usp=sharing): ETH Zürich (Team Digital Twin) +- [Challenge 1](https://drive.google.com/file/d/15ywkJPAj2DExlBq-6XFlsraUkESm8Ffw/view?usp=sharing): ETH Zürich (Team Digital Twin) - [Challenge 2](https://drive.google.com/file/d/17fWn1r2PuiRY4SsZa3t4J1bbLN9bujW7/view?usp=sharing): Esri (Team R&D Center Zurich) - [Challenge 3](https://drive.google.com/file/d/15utZHOGJumu9RhHr4l6r3nDqQBL3sx-q/view?usp=sharing): schaerholzbau ag, Gumpp & Maier GmbH and Berner Fachhochschule BFH - [Challenge 4](https://drive.google.com/file/d/168HNuN24a5jYcX_qmBpMhI-ScWgcYPyn/view?usp=sharing): Balteschwiler AG, LIGNOCAM, Cadwork, BORM-INFORMATIK GmbH and Technowood @@ -67,14 +67,16 @@ February 07-09, 2025 ## Organiser -This event is proudly organised by opensource.construction together with the ZHAW Institute of Building Technology and Process (host institution) and the Institute for Digital Construction and Wood Industry from Berner Fachhochschule (key partner). +This event is proudly organised by opensource.construction together with the ZHAW Institute of Building Technology and Process (host institution) and the Institute for Digital Construction and Wood Industry from Berner Fachhochschule (key partner). ## Location

The event will take place in Halle 180 from ZHAW in Winterthur.

-Exact adress: [Tössfeldstrasse 11 / 8400 Winterthur – Switzerland](https://maps.app.goo.gl/X8fsm29V4QdsLY8X8) +Exact adress: [Tössfeldstrasse 11 / 8400 Winterthur – +Switzerland](https://maps.app.goo.gl/X8fsm29V4QdsLY8X8) ## Language + Main language of the event is english. Depending on whom you talk to, many other languages will be spoken as well between the participants. ## Contact Us @@ -82,4 +84,3 @@ Main language of the event is english. Depending on whom you talk to, many other For more information about sponsorship or partnership opportunities, please contact Max at [max@opensource.construction](mailto:max@opensource.construction). We look forward to seeing you in Winterthur for a weekend of innovation, collaboration, and groundbreaking solutions! - From d48886a020fd091251c89492e623f9cd45da3b0d Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:39:26 +0100 Subject: [PATCH 24/33] Remove outdated network partners from JSON file --- content/partners/network-partners.json | 79 -------------------------- 1 file changed, 79 deletions(-) diff --git a/content/partners/network-partners.json b/content/partners/network-partners.json index bd59ce7..2f716d5 100644 --- a/content/partners/network-partners.json +++ b/content/partners/network-partners.json @@ -1,82 +1,3 @@ -[ - { - "name": "metaXD", - "logo": "metaxd.png", - "url": "https://www.metaxd.ch/", - "type": "industry", - "tier": "network_partner" - }, - { - "name": "2050 materials", - "logo": "2050materials.png", - "url": "https://2050-materials.com/", - "type": "industry", - "tier": "network_partner" - }, - { - "name": "Pirmin Jung", - "logo": "pirminjung.png", - "url": "https://www.pirminjung.ch/", - "type": "industry", - "tier": "network_partner" - }, - { - "name": "Balteschwiler", - "logo": "balteschwiler.png", - "url": "https://balteschwiler.ch/", - "type": "industry", - "tier": "network_partner" - }, - { - "name": "Borm", - "logo": "borm.png", - "url": "https://www.borm.com/", - "type": "industry", - "tier": "network_partner" - }, - { - "name": "lignocam", - "logo": "lignocam.jpg", - "url": "https://lignocam.com/", - "type": "industry", - "tier": "network_partner" - }, - { - "name": "Schär Holzbau", - "logo": "schaer.png", - "url": "https://schaerholzbau.ch/", - "type": "industry", - "tier": "network_partner" - }, - { - "name": "DesignToProduction", - "logo": "dtp.png", - "url": "https://www.designtoproduction.com/", - "type": "industry", - "tier": "network_partner" - }, - { - "name": "Cadwork", - "logo": "cadwork.png", - "url": "https://www.cadwork.com/", - "type": "industry", - "tier": "network_partner" - }, - { - "name": "Gumpp & Maier", - "logo": "gumppmaier.png", - "url": "https://www.gumpp-maier.de/", - "type": "industry", - "tier": "network_partner" - }, - { - "name": "Penzel Valier", - "logo": "penzelvalier.png", - "url": "https://www.penzelvalier.ch/", - "type": "industry", - "tier": "network_partner" - } -] [ { "name": "metaXD", From 30fe4779414a82125bf69f469fe31dc3aecd7287 Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:49:30 +0100 Subject: [PATCH 25/33] Add duration calculation for events --- lib/mdxParser/mdxMappers.ts | 24 ++++++++++++++++++++++-- lib/mdxParser/mdxParser.ts | 3 --- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/mdxParser/mdxMappers.ts b/lib/mdxParser/mdxMappers.ts index 768b58a..79f8713 100644 --- a/lib/mdxParser/mdxMappers.ts +++ b/lib/mdxParser/mdxMappers.ts @@ -113,6 +113,16 @@ type MetadataTransformer = { ) => ContentTypeMap[K]["metadata"]; }; +const getDurationInMinutes = (duration: any): number => { + if (!duration) return 0; + + const days = duration.days || 0; + const hours = duration.hours || 0; + const minutes = duration.minutes || 0; + + return (days * 24 * 60) + (hours * 60) + minutes; +}; + const metadataTransforms: MetadataTransformer = { project: (raw, defaultContent) => ({ ...defaultContent.metadata, @@ -132,11 +142,21 @@ const metadataTransforms: MetadataTransformer = { const eventStart = ensureDate( raw?.event?.start || defaultContent.metadata.start, ); - const isPast = eventStart.getTime() < Date.now(); + + const duration = raw?.event?.duration; + const durationInMinutes = getDurationInMinutes(duration); + + const eventEnd = durationInMinutes > 0 + ? new Date(eventStart.getTime() + durationInMinutes * 60 * 1000) + : eventStart; + + const isPast = eventEnd.getTime() < Date.now(); + + return { ...defaultContent.metadata, start: eventStart, - isPast: eventStart.getTime() < Date.now(), + isPast, duration: raw?.event?.duration || undefined, location: raw?.event?.location || undefined, geo: raw?.event?.geo diff --git a/lib/mdxParser/mdxParser.ts b/lib/mdxParser/mdxParser.ts index 901cc43..e856283 100644 --- a/lib/mdxParser/mdxParser.ts +++ b/lib/mdxParser/mdxParser.ts @@ -49,9 +49,6 @@ export function loadContent( >; const defaultContent = contentDefaults[type] as ContentTypeMap[T]; - if (dir === "page") { - console.log("parsed", parsed?.data); - } const parsedContent = parsed ? validator(parsed.data, slug, parsed.body, defaultContent) From 1cc22a59115c8cc1806d730d3444a6f5b6c2b777 Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:59:13 +0100 Subject: [PATCH 26/33] Add URL validation for button component links --- components/src/button/button.tsx | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/components/src/button/button.tsx b/components/src/button/button.tsx index 23eced9..d3c2615 100644 --- a/components/src/button/button.tsx +++ b/components/src/button/button.tsx @@ -6,6 +6,18 @@ import Link from "next/link"; import { MouseEventHandler } from "react"; import { useFormStatus } from "react-dom"; +const isValidUrl = (url: string): boolean => { + if (!url) return false; + // Allow relative paths + if (url.startsWith("/")) return true; + try { + const parsed = new URL(url); + return ["http:", "https:"].includes(parsed.protocol); + } catch { + return false; + } +}; + export const button = cva( ["inline-block pr-3 md:pr-8 text-sm font-bold no-underline md:text-base"], { @@ -68,6 +80,9 @@ export const Button = ({ }) => { const { pending } = useFormStatus(); + const sanitizedHref = + typeof target === "string" ? (isValidUrl(target) ? target : "/") : ""; + return (
{type === "submit" ? ( @@ -89,7 +104,11 @@ export const Button = ({ ) : ( Date: Fri, 7 Feb 2025 09:12:41 +0100 Subject: [PATCH 27/33] Refactor events components to improve structure and readability --- app/page.tsx | 1 + components/src/partials/events.tsx | 41 +++++++++++++++--------------- lib/mdxParser/mdxMappers.ts | 1 - 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index f44eb44..acb3dfe 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -11,6 +11,7 @@ import Image from "next/image"; import team from "../content/team.json"; import ContactCard from "@/components/src/contactCard"; import { PartnersPartial } from "@/components/src/partials/partners"; +import { EventsIndexPartial } from "@/components/src/partials/events"; export default function Home() { const discordLink = process.env.DISCORD_LINK || ""; diff --git a/components/src/partials/events.tsx b/components/src/partials/events.tsx index a3176d5..b86ce3e 100644 --- a/components/src/partials/events.tsx +++ b/components/src/partials/events.tsx @@ -2,10 +2,6 @@ import { Button } from "../button"; import { Card } from "../card"; import { loadEvents } from "@/lib/mdxParser/mdxParser"; -const isPastEvent = (date: Date) => { - return date.getTime() < Date.now(); -}; - export function EventsPartial({ showPast = false }: { showPast?: boolean }) { let eventsParsed = loadEvents(); @@ -17,19 +13,24 @@ export function EventsPartial({ showPast = false }: { showPast?: boolean }) { ); return ( -
- {!es.length - ? "No pending events" - : es.map((e) => ( - - ))} +
+

+ {showPast ? "Past Events" : "Upcoming Events"} +

+
+ {!es.length + ? "No pending events" + : es.map((e) => ( + + ))} +
); } @@ -38,12 +39,12 @@ export function EventsIndexPartial() { let eventsParsed = loadEvents(); const pastEvents = eventsParsed - .filter((e) => isPastEvent(e.metadata.start)) + .filter((e) => e.metadata.isPast) .sort((a, b) => (a.metadata.start < b.metadata.start ? 1 : -1)) .slice(0, 3); const futureEvents = eventsParsed - .filter((e) => !isPastEvent(e.metadata.start)) + .filter((e) => !e.metadata.isPast) .sort((a, b) => (a.metadata.start < b.metadata.start ? 1 : -1)); return ( @@ -101,7 +102,7 @@ export function EventsIndexPartial() {
diff --git a/lib/mdxParser/mdxMappers.ts b/lib/mdxParser/mdxMappers.ts index 79f8713..feef9e3 100644 --- a/lib/mdxParser/mdxMappers.ts +++ b/lib/mdxParser/mdxMappers.ts @@ -152,7 +152,6 @@ const metadataTransforms: MetadataTransformer = { const isPast = eventEnd.getTime() < Date.now(); - return { ...defaultContent.metadata, start: eventStart, From 43dc3b43d81374a2568ad98a869f73700241d0ac Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Fri, 28 Feb 2025 18:55:08 +0100 Subject: [PATCH 28/33] Refactor cluster mapping for improved readability --- app/clusters/page.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/clusters/page.tsx b/app/clusters/page.tsx index ea345f3..f5623f6 100644 --- a/app/clusters/page.tsx +++ b/app/clusters/page.tsx @@ -11,13 +11,13 @@ export default function ClusterPage() {
- {clusters.map((c, index) => ( + {clusters.map((cluster, index) => ( ))}
From 361f19560e0bf88669c700fea53ae1ccc791fb3a Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Fri, 28 Feb 2025 18:57:58 +0100 Subject: [PATCH 29/33] Add Xeokit project to project map --- content/project-map.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/content/project-map.json b/content/project-map.json index 99c6f89..580ebb8 100644 --- a/content/project-map.json +++ b/content/project-map.json @@ -82,5 +82,11 @@ "slug": "BetterCorrectFast", "featured": false, "maturity": "sandbox" + }, + { + "title": "Xeokit", + "slug": "xeokit", + "featured": false, + "maturity": "graduated" } ] From 5a820d53837731a3f82ee4f996c200ea1ef3a6eb Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Fri, 28 Feb 2025 19:06:25 +0100 Subject: [PATCH 30/33] Remove project map and update metadata for BetterCorrectFast and xeokit projects --- content/project-map.json | 92 -------------------------- content/projects/BetterCorrectFast.mdx | 7 +- content/projects/xeokit.mdx | 24 +++++-- 3 files changed, 25 insertions(+), 98 deletions(-) delete mode 100644 content/project-map.json diff --git a/content/project-map.json b/content/project-map.json deleted file mode 100644 index 580ebb8..0000000 --- a/content/project-map.json +++ /dev/null @@ -1,92 +0,0 @@ -[ - { - "title": "Bonsai", - "slug": "bonsai", - "featured": true, - "maturity": "graduated" - }, - { - "title": "Compas", - "slug": "compas", - "featured": true, - "maturity": "graduated" - }, - { - "title": "IFC Model Checker", - "slug": "ifc-model-checker", - "featured": true, - "maturity": "sandbox" - }, - { - "title": "PyRevit", - "slug": "pyrevit", - "featured": false, - "maturity": "graduated" - }, - { - "title": "Sprint", - "slug": "sprint", - "featured": false, - "maturity": "incubation" - }, - { - "title": "Vyssuals", - "slug": "vyssuals", - "featured": false, - "maturity": "incubation" - }, - { - "title": "Circular Construction Co-Pilot", - "slug": "circular-construction-co-pilot", - "featured": false, - "maturity": "sandbox" - }, - { - "title": "IFC LCA", - "slug": "ifc-lca", - "featured": false, - "maturity": "incubation" - }, - { - "title": "LCAX and EPDX", - "slug": "lcax-and-epdx", - "featured": false, - "maturity": "sandbox" - }, - { - "title": "Speckle", - "slug": "speckle", - "featured": false, - "maturity": "graduated" - }, - { - "title": "That Open Company", - "slug": "that-open-company", - "featured": false, - "maturity": "graduated" - }, - { - "title": "Calc", - "slug": "calc", - "featured": false, - "maturity": "incubation" - }, - { - "title": "Share by bldrs.ai", - "slug": "bldrs", - "featured": false, - "maturity": "sandbox" - }, - { - "title": "BetterCorrectFast", - "slug": "BetterCorrectFast", - "featured": false, - "maturity": "sandbox" - }, - { - "title": "Xeokit", - "slug": "xeokit", - "featured": false, - "maturity": "graduated" - } -] diff --git a/content/projects/BetterCorrectFast.mdx b/content/projects/BetterCorrectFast.mdx index 005ce12..1530d63 100644 --- a/content/projects/BetterCorrectFast.mdx +++ b/content/projects/BetterCorrectFast.mdx @@ -1,6 +1,9 @@ --- title: BetterCorrectFast description: A python library for simplified BIM Collaboration Format (BCF) generation +metadata: + featured: false + maturity: sandbox links: - url: https://github.com/boydhont/BetterCorrectFast label: GitHub @@ -23,6 +26,7 @@ BetterCorrectFast has been built on top of the findings of other open-source pro Python3 ### License + MIT License ### Operating Model @@ -34,4 +38,5 @@ Self-sustained: Voluntary contributions are welcome. The project came to life in a collaboration between [Krzysztof Gorczakowski](https://www.linkedin.com/in/kgorczakowski/), [Grzegorz Wil](https://www.linkedin.com/in/bimlab-grzegorz-wilk/) and [Boy d'Hont](https://www.linkedin.com/in/boydhont/) at the [AEC Hackaton Wroclaw 2024](https://hack.creoox.com/) and is currently actively maintained by [Boy d'Hont](https://www.linkedin.com/in/boydhont/). ### Contact -For more info, please reach out to Boy d'Hont via mail: contact@bdhont.net + +For more info, please reach out to Boy d'Hont via mail: contact@bdhont.net diff --git a/content/projects/xeokit.mdx b/content/projects/xeokit.mdx index b5d3ce4..cad540d 100644 --- a/content/projects/xeokit.mdx +++ b/content/projects/xeokit.mdx @@ -1,33 +1,42 @@ --- title: xeokit description: Graphics SDK for Browser-based BIM and AEC Visualization +metadata: + featured: false + maturity: graduated links: - url: https://xeokit.io/ label: Website - url: https://github.com/xeokit label: GitHub --- + xeokit is an open-source 3D web graphics SDK from xeolabs & Creoox for BIM and AEC. Built to view huge models in the browser. Used by industry leaders. ### Problem + The AEC industry faces significant challenges in efficiently visualizing and managing **complex 3D models and BIM data in web** environments. Many existing solutions are resource-intensive, lack support for multiple file formats, or require extensive technical expertise for integration. In addition, concerns are raised about **data ownership, security and privacy**. ### Solution + xeokit SDK is a **high-performance, open-source toolkit** designed for **3D BIM and engineering model client-based visualization** - directly in the browser. You know what happens with your data at all times! Unlike traditional solutions that are resource-heavy, format-restrictive, or require complex server-side setups, xeokit SDK enables **lightweight, efficient, and interactive visualization** without additional software installations. **Built for the AEC industry** specifically, xeokit SDK supports **large-scale models with high precision**, making it ideal for architects, engineers, and developers who need a seamless and customizable way to integrate 3D BIM data into their web applications. Key features include: -- **High-performance** rendering of large models directly in the browser, enabled by a very compact internal geometry format. -- Support for **Multiple File Formats**: Compatible with various model formats such as IFC (2x3 and 4.3), glTF, OBJ, STL, 3DXML, LAZ/LAS, CityJSON, and XKT, allowing for versatile model integration. -- **Double-Precision Geometry**: Handles models with double-precision & real-world coordinates, ensuring accurate rendering without loss of precision, even with federated models. -- **Extensible JavaScript Toolkit**: Offers an extensive and growing library of plugins and tools to accelerate application development, including navigation aids (plan views, section planes, FPN), measurement tools (distance and angle measurement), collaboration features (3D annotations, BCF viewpoints), and various model loaders. For the quick start there is even a ready-to-use simple BIM Viewer. -With these capabilities, xeokit SDK empowers AEC professionals to interact with and analyse their data more efficiently. + +- **High-performance** rendering of large models directly in the browser, enabled by a very compact internal geometry format. +- Support for **Multiple File Formats**: Compatible with various model formats such as IFC (2x3 and 4.3), glTF, OBJ, STL, 3DXML, LAZ/LAS, CityJSON, and XKT, allowing for versatile model integration. +- **Double-Precision Geometry**: Handles models with double-precision & real-world coordinates, ensuring accurate rendering without loss of precision, even with federated models. +- **Extensible JavaScript Toolkit**: Offers an extensive and growing library of plugins and tools to accelerate application development, including navigation aids (plan views, section planes, FPN), measurement tools (distance and angle measurement), collaboration features (3D annotations, BCF viewpoints), and various model loaders. For the quick start there is even a ready-to-use simple BIM Viewer. + With these capabilities, xeokit SDK empowers AEC professionals to interact with and analyse their data more efficiently. ### Why Open Source? + By adopting an open-source model, xeokit SDK promotes **transparency, trust, flexibility, and innovation** within the AEC industry. Developers and organizations worldwide can contribute to and benefit from continuous improvements, fostering a **collaborative ecosystem** that drives technological advancement. Rather than reinventing the wheel, teams can **accelerate development** by building on top of a proven, well-supported solution—delivering value faster and focusing on their core innovations. ### Technology + - Pure **WebGL** for hardware-accelerated 3D rendering - **JavaScript/TypeScript** for a flexible and extensible API - Custom **XKT format** for ultra-fast loading of large BIM models @@ -37,19 +46,24 @@ Rather than reinventing the wheel, teams can **accelerate development** by build - **Distributed via npm and GitHub**, making it easy to integrate into any development workflow ### License + xeokit SDK is distributed under the **Affero GPLv3** License, ensuring that all modifications and derivative works remain open source when distributed. This commitment to openness enhances transparency and collaboration within the community. ### Operating Model + xeokit SDK is a partly **community-driven**, partly **industry-driven** open-source project with **commercial backing from Creoox AG** (https://creoox.com/). While the source code is freely available under Affero GPLv3, organizations can access **commercial support, alternative license, custom integrations, and additional services** to maximize the SDK’s potential. Typical xeokit SDK customers are software providers in the field of CDE platforms, Facility Management, Construction Management, IoT and Infrastructure. ### About the team + xeokit SDK was originally developed by **Lindsay Kay** and is now actively maintained by a **dedicated team of software engineers, xeokit integrators, and AEC industry experts at Creoox AG**, along with contributions from a **growing community of innovators**. With a strong focus on **technical excellence, open collaboration, and industry-driven innovation**, the team is committed to advancing 3D/BIM technology and empowering AEC professionals worldwide with cutting-edge visualization solutions. ### Contact + For more information or collaboration opportunities, reach out via email at contact@creoox.com or visit our website: xeokit.io. ### Image / Video Footage + https://www.youtube.com/watch?v=YhVSu5pOsaQ&t=180s https://www.youtube.com/watch?v=Q9pukZc-6eA … From ee74bf85d457ab59def909ac7468f817d92382fb Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Thu, 6 Mar 2025 11:20:50 +0100 Subject: [PATCH 31/33] fix button --- components/src/button/button.tsx | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/components/src/button/button.tsx b/components/src/button/button.tsx index 23caf4e..0d43950 100644 --- a/components/src/button/button.tsx +++ b/components/src/button/button.tsx @@ -15,18 +15,6 @@ const isValidUrl = (url: string): boolean => { } }; -const isValidUrl = (url: string): boolean => { - if (!url) return false; - // Allow relative paths - if (url.startsWith("/")) return true; - try { - const parsed = new URL(url); - return ["http:", "https:"].includes(parsed.protocol); - } catch { - return false; - } -}; - export const button = cva( ["inline-block pr-3 md:pr-8 text-sm font-bold no-underline md:text-base"], { @@ -92,9 +80,6 @@ export const Button = ({ const sanitizedHref = typeof target === "string" ? (isValidUrl(target) ? target : "/") : ""; - const sanitizedHref = - typeof target === "string" ? (isValidUrl(target) ? target : "/") : ""; - return (
{type === "submit" ? ( From 130d43b13db1bc84a21381a5152e9d02aa7304dd Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Fri, 14 Mar 2025 07:45:28 +0100 Subject: [PATCH 32/33] remove vite-tsconfig-paths dependency and delete vite.config.ts --- package-lock.json | 55 ++++++----------------------------------------- package.json | 1 - vite.config.ts | 15 ------------- 3 files changed, 6 insertions(+), 65 deletions(-) delete mode 100644 vite.config.ts diff --git a/package-lock.json b/package-lock.json index 58df203..3eb247b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,6 @@ "react-dom": "^18", "react-icons": "^5.2.1", "slugify": "^1.6.6", - "vite-tsconfig-paths": "^5.1.4", "yaml": "^2.4.2", "zod": "^3.23.8" }, @@ -9998,7 +9997,7 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -11745,11 +11744,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" - }, "node_modules/google-auth-library": { "version": "9.14.1", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.14.1.tgz", @@ -18541,7 +18535,7 @@ "version": "8.4.49", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "opencollective", @@ -20344,7 +20338,7 @@ "version": "4.28.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", - "devOptional": true, + "dev": true, "dependencies": { "@types/estree": "1.0.6" }, @@ -20382,7 +20376,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "devOptional": true + "dev": true }, "node_modules/rrweb-cssom": { "version": "0.7.1", @@ -22152,25 +22146,6 @@ } } }, - "node_modules/tsconfck": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.4.tgz", - "integrity": "sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==", - "bin": { - "tsconfck": "bin/tsconfck.js" - }, - "engines": { - "node": "^18 || >=20" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -22417,7 +22392,7 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", - "devOptional": true, + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -23191,7 +23166,7 @@ "version": "5.4.14", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", @@ -23270,24 +23245,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/vite-tsconfig-paths": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", - "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", - "dependencies": { - "debug": "^4.1.1", - "globrex": "^0.1.2", - "tsconfck": "^3.0.3" - }, - "peerDependencies": { - "vite": "*" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, "node_modules/vitest": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", diff --git a/package.json b/package.json index 2524319..bf90729 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "react-dom": "^18", "react-icons": "^5.2.1", "slugify": "^1.6.6", - "vite-tsconfig-paths": "^5.1.4", "yaml": "^2.4.2", "zod": "^3.23.8" }, diff --git a/vite.config.ts b/vite.config.ts deleted file mode 100644 index e8553a6..0000000 --- a/vite.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from "vite"; -import path from "path"; - -export default defineConfig({ - test: { - globals: true, - }, - resolve: { - alias: { - "@": path.resolve(__dirname, "./"), - "@lib": path.resolve(__dirname, "./lib"), - "@components": path.resolve(__dirname, "./components"), - }, - }, -}); From 85d5751d23a5788766d1de5a436d69a49f1417e8 Mon Sep 17 00:00:00 2001 From: Felix Brunold <48569186+TheVessen@users.noreply.github.com> Date: Fri, 14 Mar 2025 07:57:27 +0100 Subject: [PATCH 33/33] Add vitest configuration and fix key prop in EventsIndexPartial --- components/src/partials/events.tsx | 6 ++++-- vitest.config.ts | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 vitest.config.ts diff --git a/components/src/partials/events.tsx b/components/src/partials/events.tsx index b86ce3e..f4340e9 100644 --- a/components/src/partials/events.tsx +++ b/components/src/partials/events.tsx @@ -80,9 +80,11 @@ export function EventsIndexPartial() {
{pastEvents.map((e) => ( -
+