import type { MarkdownRoot } from "@nuxt/content"; import gsap from "gsap"; type MarkdownBody = ( | { type: "h1" | "p"; text: string; } | { type: "img"; image: HTMLImageElement; } )[]; const createImage = (src: string): HTMLImageElement => { // TODO: how to cleanup ? const img = document.createElement("img"); img.src = src; return img; }; // TODO: move to utils or smth maybe const simplifyMarkdownAST = (root: MarkdownRoot) => { const body: MarkdownBody = []; for (const node of root.value) { if (Array.isArray(node)) { const [type, props, value] = node; console.log("--------------"); console.log(`type = ${type}`); console.log(`props = ${JSON.stringify(props)}`); console.log(`value = ${value}`); switch (type) { case "h1": case "p": { if (typeof value !== "string") throw `Unsupported node value for '${type}': '${value}'`; body.push({ type, text: value }); break; } case "img": { if (typeof props["src"] !== "string") throw `Unsupported type or missing node prop "src" for '${type}': '${JSON.stringify(props)}'`; body.push({ type, image: createImage(props.src) }); break; } default: { throw `Unsupported '${type}'`; } } } else { throw `Unsupported node kind 'string'`; } } return body; }; export const useProjectsStore = defineStore("projects", { state: () => ({ projects: [] as { description: string; thumbnail: string; preview: string; url: string | null; body: MarkdownBody; }[], currentProject: 0, loading: true, offsetX: 0, }), actions: { async loadProjects() { this.loading = true; const { data: projects } = await useAsyncData("projects", () => queryCollection("projects").order("order", "ASC").all(), ); if (!projects.value) throw "Cannot load projects"; this.projects = []; for (const project of projects.value) { const parts = project.id.replace(".md", "").split("/"); const id = parts[parts.length - 1]!; this.projects.push({ description: project.description, thumbnail: `/images/projects/thumbnails/${id}.webp`, preview: `/images/projects/previews/${id}.webp`, url: project.url, body: simplifyMarkdownAST(project.body), }); } this.loading = false; }, visitProject() { const url = this.projects[this.currentProject]!.url; if (url) navigateTo(url, { external: true, open: { target: "_blank" } }); }, scrollProjects(direction: "left" | "right") { let offset = 0; if ( direction === "right" && this.currentProject < this.projects.length - 1 ) { this.currentProject += 1; offset = 69; } else if (direction === "left" && this.currentProject > 0) { this.currentProject -= 1; offset = -69; } if (offset !== 0) { gsap.fromTo( this, { offsetX: offset }, { offsetX: 0, duration: 0.1, ease: "none", }, ); } }, scrollToProject(index: number) { if (index === this.currentProject) return; const offset = (index - this.currentProject) * 69; this.currentProject = index; gsap.fromTo( this, { offsetX: offset }, { offsetX: 0, duration: 0.1, ease: "none", }, ); }, }, });