From b97983af986bc46359c1926c15c315931dd7a347 Mon Sep 17 00:00:00 2001 From: Thorsten Seyschab Date: Sat, 7 Mar 2026 17:44:05 +0100 Subject: [PATCH 01/11] feat: add error page --- app/error.vue | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100755 app/error.vue diff --git a/app/error.vue b/app/error.vue new file mode 100755 index 0000000..e8bfd14 --- /dev/null +++ b/app/error.vue @@ -0,0 +1,44 @@ + + + From 1e94f3048af7add8c304be1041181f3c9fa2f6b7 Mon Sep 17 00:00:00 2001 From: Thorsten Seyschab Date: Sat, 7 Mar 2026 17:59:34 +0100 Subject: [PATCH 02/11] feat: add landing page --- app/composables/useAllTestimonials.ts | 38 +++++++ app/pages/index.vue | 157 ++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100755 app/composables/useAllTestimonials.ts create mode 100755 app/pages/index.vue diff --git a/app/composables/useAllTestimonials.ts b/app/composables/useAllTestimonials.ts new file mode 100755 index 0000000..881dc65 --- /dev/null +++ b/app/composables/useAllTestimonials.ts @@ -0,0 +1,38 @@ +export interface LinkedTestimonial { + quote: string + author: string + role: string + linkTo: string +} + +export function useAllTestimonials() { + return useAsyncData('all-testimonials', async () => { + const [clients, projects, talks] = await Promise.all([ + queryCollection('clients').all(), + queryCollection('projects').all(), + queryCollection('talks').all(), + ]) + + const result: LinkedTestimonial[] = [] + + for (const p of clients) { + for (const t of p.testimonials ?? []) { + result.push({ ...t, linkTo: p.path }) + } + } + + for (const p of projects) { + for (const t of p.testimonials ?? []) { + result.push({ ...t, linkTo: p.path }) + } + } + + for (const talk of talks) { + for (const t of talk.testimonials ?? []) { + result.push({ ...t, linkTo: talk.path }) + } + } + + return result + }) +} diff --git a/app/pages/index.vue b/app/pages/index.vue new file mode 100755 index 0000000..2ac6197 --- /dev/null +++ b/app/pages/index.vue @@ -0,0 +1,157 @@ + + + From b3fb7a250e0fd0158b5726835e705ddc87aea332 Mon Sep 17 00:00:00 2001 From: Thorsten Seyschab Date: Sat, 7 Mar 2026 18:08:45 +0100 Subject: [PATCH 03/11] feat: new landing page block order and new block for recent projects --- app/composables/useSortedProjects.ts | 32 ++++++++++++++ app/pages/index.vue | 66 ++++++++++++++++++---------- app/pages/projects/index.vue | 23 +--------- 3 files changed, 77 insertions(+), 44 deletions(-) create mode 100644 app/composables/useSortedProjects.ts diff --git a/app/composables/useSortedProjects.ts b/app/composables/useSortedProjects.ts new file mode 100644 index 0000000..a48f925 --- /dev/null +++ b/app/composables/useSortedProjects.ts @@ -0,0 +1,32 @@ +/** + * Fetches all projects and returns them sorted by recency. + * Sort order: ongoing first, then by endDate, startDate, and stars. + * @param limit - Optional max number of projects to return. + */ +export function useSortedProjects(limit?: number) { + const key = limit ? `sorted-projects-${limit}` : 'sorted-projects' + + return useAsyncData(key, async () => { + const projects = await queryCollection('projects').all() + const sorted = projects.sort((a, b) => { + // 1. Ongoing (no endDate) before completed + const aOngoing = a.endDate == null + const bOngoing = b.endDate == null + if (aOngoing !== bOngoing) return aOngoing ? -1 : 1 + + // 2. Within completed: newest endDate first + if (!aOngoing && !bOngoing) { + const endCmp = b.endDate!.localeCompare(a.endDate!) + if (endCmp !== 0) return endCmp + } + + // 3. Newest startDate first + const startCmp = b.startDate.localeCompare(a.startDate) + if (startCmp !== 0) return startCmp + + // 4. More stars first + return (b.repoStars ?? 0) - (a.repoStars ?? 0) + }) + return limit ? sorted.slice(0, limit) : sorted + }) +} diff --git a/app/pages/index.vue b/app/pages/index.vue index 2ac6197..522eadd 100755 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -20,12 +20,14 @@ const skills = [ const [ { data: recentTalks }, + { data: recentProjects }, { data: featuredTestimonials }, { data: socials }, ] = await Promise.all([ await useAsyncData('recent-talks', () => queryCollection('talks').order('date', 'DESC').limit(3).all(), ), + await useSortedProjects(2), await useAllTestimonials(), await useAsyncData('index-socials-all', () => queryCollection('socials').where('active', '=', true).order('sortOrder', 'ASC').all(), @@ -91,8 +93,32 @@ const [

- + + +
+ +
+ +
+
+ {{ social.name }} +
+
+ {{ social.handle }} +
+
+
+
+
+
+ + + @@ -130,27 +156,23 @@ const [ - - - -
- + + +
+

+ Recent Projects +

+ -
- -
-
- {{ social.name }} -
-
- {{ social.handle }} -
-
-
- + View all projects + +
+
+
+
diff --git a/app/pages/projects/index.vue b/app/pages/projects/index.vue index 1bed718..2affdec 100755 --- a/app/pages/projects/index.vue +++ b/app/pages/projects/index.vue @@ -9,28 +9,7 @@ defineOgImageComponent('Project', { description: 'Tools, experiments, and applications for 3D on the web, Vue/Nuxt, and developer tooling.', }) -const { data: projects } = await useAsyncData('projects', async () => { - const projects = await queryCollection('projects').all() - return projects.sort((a, b) => { - // 1. Ongoing (no endDate) before completed - const aOngoing = a.endDate == null - const bOngoing = b.endDate == null - if (aOngoing !== bOngoing) return aOngoing ? -1 : 1 - - // 2. Within completed: newest endDate first - if (!aOngoing && !bOngoing) { - const endCmp = b.endDate!.localeCompare(a.endDate!) - if (endCmp !== 0) return endCmp - } - - // 3. Newest startDate first - const startCmp = b.startDate.localeCompare(a.startDate) - if (startCmp !== 0) return startCmp - - // 4. More stars first - return (b.repoStars ?? 0) - (a.repoStars ?? 0) - }) -}) +const { data: projects } = await useSortedProjects()