feat: render semantic portfolio document
This commit is contained in:
64
src/components/AppShell.vue
Normal file
64
src/components/AppShell.vue
Normal 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>
|
||||
20
src/components/ContactDestination.vue
Normal file
20
src/components/ContactDestination.vue
Normal 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>
|
||||
30
src/components/ExperienceTimeline.vue
Normal file
30
src/components/ExperienceTimeline.vue
Normal 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>
|
||||
33
src/components/InfrastructureDiagram.vue
Normal file
33
src/components/InfrastructureDiagram.vue
Normal 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>
|
||||
16
src/components/PortfolioSection.vue
Normal file
16
src/components/PortfolioSection.vue
Normal 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>
|
||||
15
src/components/ProjectCard.vue
Normal file
15
src/components/ProjectCard.vue
Normal 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>
|
||||
27
src/components/ProjectCaseStudy.vue
Normal file
27
src/components/ProjectCaseStudy.vue
Normal 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>
|
||||
15
src/components/SiteNavigation.vue
Normal file
15
src/components/SiteNavigation.vue
Normal 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>
|
||||
19
src/components/ToolkitGrid.vue
Normal file
19
src/components/ToolkitGrid.vue
Normal 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>
|
||||
Reference in New Issue
Block a user