feat: render semantic portfolio document

This commit is contained in:
2026-09-02 01:17:45 +03:00
parent bc851e398b
commit 7d136ef16d
17 changed files with 488 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
<script setup lang="ts">
import ContactDestination from './ContactDestination.vue';
import ExperienceTimeline from './ExperienceTimeline.vue';
import InfrastructureDiagram from './InfrastructureDiagram.vue';
import PortfolioSection from './PortfolioSection.vue';
import ProjectCard from './ProjectCard.vue';
import ProjectCaseStudy from './ProjectCaseStudy.vue';
import SiteNavigation from './SiteNavigation.vue';
import ToolkitGrid from './ToolkitGrid.vue';
import type { PortfolioContent } from '@/content/types';
defineProps<{ content: Readonly<PortfolioContent> }>();
</script>
<template>
<a class="skip-link" href="#portfolio-content">Skip to portfolio content</a>
<header>
<h1>{{ content.identity.name }}</h1>
<p>{{ content.identity.role }}</p>
<SiteNavigation :items="content.navigation" />
</header>
<main id="portfolio-content">
<PortfolioSection id="start" :index="1" :eyebrow="content.navigation[0]!.label.toUpperCase()" :heading="content.navigation[0]!.label.toUpperCase()">
<p>{{ content.identity.valueProposition }}</p>
</PortfolioSection>
<PortfolioSection id="profile" :index="2" :eyebrow="content.navigation[1]!.label.toUpperCase()" :heading="content.navigation[1]!.label.toUpperCase()">
<p>{{ content.profile.introduction }}</p>
<dl>
<template v-for="fact in content.profile.screeningFacts" :key="fact.label">
<dt>{{ fact.label }}</dt>
<dd>{{ fact.value }}</dd>
</template>
</dl>
</PortfolioSection>
<PortfolioSection id="experience" :index="3" :eyebrow="content.navigation[2]!.label.toUpperCase()" :heading="content.navigation[2]!.label.toUpperCase()">
<ExperienceTimeline :experience="content.experience" :systems-operations="content.systemsOperations" />
</PortfolioSection>
<PortfolioSection id="infrastructure" :index="4" :eyebrow="content.navigation[3]!.label.toUpperCase()" :heading="content.navigation[3]!.label.toUpperCase()">
<InfrastructureDiagram :infrastructure="content.infrastructure" />
</PortfolioSection>
<PortfolioSection id="selected-builds" :index="5" :eyebrow="content.navigation[4]!.label.toUpperCase()" :heading="content.navigation[4]!.label.toUpperCase()">
<ProjectCaseStudy v-for="project in content.projects.selected" :key="project.id" :project="project" />
</PortfolioSection>
<PortfolioSection id="additional-work" :index="6" :eyebrow="content.navigation[5]!.label.toUpperCase()" :heading="content.navigation[5]!.label.toUpperCase()">
<ProjectCard v-for="project in content.projects.additional" :key="project.id" :project="project" />
</PortfolioSection>
<PortfolioSection id="toolkit" :index="7" :eyebrow="content.navigation[6]!.label.toUpperCase()" :heading="content.navigation[6]!.label.toUpperCase()">
<ToolkitGrid :groups="content.toolkit" />
</PortfolioSection>
<PortfolioSection id="contact" :index="8" :eyebrow="content.navigation[7]!.label.toUpperCase()" :heading="content.navigation[7]!.label.toUpperCase()">
<footer>
<ContactDestination :contact="content.contact" :identity="content.identity" />
</footer>
</PortfolioSection>
</main>
</template>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import type { ContactContent, PortfolioContent } from '@/content/types';
defineProps<{
contact: Readonly<ContactContent>;
identity: Readonly<Pick<PortfolioContent['identity'], 'name' | 'role'>>;
}>();
</script>
<template>
<p>{{ identity.name }}</p>
<p>{{ identity.role }}</p>
<a :href="`mailto:${contact.email}`">{{ contact.email }}</a>
<p>{{ contact.cvMessage }}</p>
<ul>
<li v-for="link in contact.social" :key="link.service">
<a :href="link.url" target="_blank" rel="noopener noreferrer">{{ link.service }}</a>
</li>
</ul>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import type { ExperienceRecord, SystemsCaseStudy } from '@/content/types';
defineProps<{
experience: readonly ExperienceRecord[];
systemsOperations: Readonly<SystemsCaseStudy>;
}>();
</script>
<template>
<ol>
<li v-for="record in experience" :key="record.id">
<article>
<h3>{{ record.role }}</h3>
<p>{{ record.organization }} · {{ record.dates }}<template v-if="record.location"> · {{ record.location }}</template></p>
<ul>
<li v-for="highlight in record.highlights" :key="highlight">{{ highlight }}</li>
</ul>
</article>
</li>
</ol>
<article>
<h3>{{ systemsOperations.title }}</h3>
<p>{{ systemsOperations.summary }}</p>
<ul>
<li v-for="item in systemsOperations.evidence" :key="item">{{ item }}</li>
</ul>
</article>
</template>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import type { InfrastructureCaseStudy } from '@/content/types';
defineProps<{ infrastructure: Readonly<InfrastructureCaseStudy> }>();
</script>
<template>
<article>
<h3>{{ infrastructure.title }}</h3>
<p>{{ infrastructure.summary }}</p>
<figure>
<svg
viewBox="0 0 800 240"
role="img"
:aria-labelledby="`${infrastructure.id}-diagram-title`"
:aria-describedby="`${infrastructure.id}-diagram-description`"
>
<title :id="`${infrastructure.id}-diagram-title`">{{ infrastructure.title }}</title>
<desc :id="`${infrastructure.id}-diagram-description`">{{ infrastructure.summary }}</desc>
<g v-for="(band, index) in infrastructure.bands" :key="band.id" :transform="`translate(${40 + index * 190} 70)`">
<rect width="150" height="100" rx="8" />
<text x="75" y="50" text-anchor="middle">{{ band.title }}</text>
</g>
</svg>
<figcaption>{{ infrastructure.title }}</figcaption>
</figure>
<ul data-testid="infrastructure-text-equivalent">
<li v-for="band in infrastructure.bands" :key="band.id">
<strong>{{ band.title }}</strong>: {{ band.summary }}
</li>
</ul>
</article>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
defineProps<{
id: string;
index: number;
eyebrow: string;
heading: string;
}>();
</script>
<template>
<section :id="id" :aria-labelledby="`${id}-heading`" :data-section-index="index">
<p>{{ String(index).padStart(2, '0') }} / {{ eyebrow }}</p>
<h2 :id="`${id}-heading`">{{ heading }}</h2>
<slot />
</section>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { PublicProject } from '@/content/types';
defineProps<{ project: Readonly<PublicProject> }>();
</script>
<template>
<article>
<h3>{{ project.title }}</h3>
<p>{{ project.summary }}</p>
<p>{{ project.contribution }}</p>
<p>{{ project.outcome }}</p>
<a v-if="project.visibility === 'public' && project.url" :href="project.url" target="_blank" rel="noopener noreferrer">{{ project.title }}</a>
</article>
</template>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import type { PublicProject } from '@/content/types';
defineProps<{ project: Readonly<PublicProject> }>();
</script>
<template>
<article>
<h3>{{ project.title }}</h3>
<p>{{ project.summary }}</p>
<dl>
<dt>Problem</dt>
<dd>{{ project.problem }}</dd>
<dt>Contribution</dt>
<dd>{{ project.contribution }}</dd>
<dt>Technical decisions</dt>
<dd>
<ul>
<li v-for="decision in project.technicalDecisions" :key="decision">{{ decision }}</li>
</ul>
</dd>
<dt>Outcome</dt>
<dd>{{ project.outcome }}</dd>
</dl>
<a v-if="project.visibility === 'public' && project.url" :href="project.url" target="_blank" rel="noopener noreferrer">{{ project.title }}</a>
</article>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { SectionId } from '@/content/types';
defineProps<{ items: ReadonlyArray<{ id: SectionId; label: string }> }>();
</script>
<template>
<nav aria-label="Portfolio sections">
<ul>
<li v-for="item in items" :key="item.id">
<a :href="`#${item.id}`">{{ item.label }}</a>
</li>
</ul>
</nav>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import type { ToolkitGroup } from '@/content/types';
defineProps<{ groups: readonly ToolkitGroup[] }>();
</script>
<template>
<div>
<section v-for="group in groups" :key="group.title" :aria-labelledby="`${group.title}-heading`">
<h3 :id="`${group.title}-heading`">{{ group.title }}</h3>
<ul>
<li v-for="technology in group.technologies" :key="technology.name" data-testid="toolkit-technology">
<span aria-hidden="true"></span>
<span>{{ technology.name }}</span>
</li>
</ul>
</section>
</div>
</template>