Initial content sections for forms case study; improvements to the layout at all breakpoints and landing on a better design

This commit is contained in:
Andrew Gioia 2026-04-18 17:20:38 -04:00
parent ebada3ff0e
commit 8020060405
Signed by: andrew
GPG Key ID: FC09694A000800C8
34 changed files with 1043 additions and 0 deletions

View File

@ -1,3 +1,17 @@
# Portfolio and case studies
Showcase of past UI/UX work by Andrew Gioia.
## Local development
Run the Hugo dev server:
```bash
hugo server -D
```
Build the site:
```bash
hugo
```

5
archetypes/default.md Normal file
View File

@ -0,0 +1,5 @@
---
title: "{{ replace .File.ContentBaseName `-` ` ` | title }}"
date: {{ .Date }}
draft: true
---

513
assets/css/site.css Normal file
View File

@ -0,0 +1,513 @@
:root {
--bg: #f0ede6;
--text: #171717;
--hover: #070707;
--muted: #676767;
--faded: #979797;
--primary: #00b0CD;
--secondary: #80c0cc;
--gray: #6f6f6f;
--pink: #E463D2;
--cyan: #00C4D8;
--green: #00D771;
--blue: #2096F6;
--orange: #E78B05;
/*--line: rgba(23, 23, 23, 0.12);*/
--line: #272727;
--light: #dfdfdf;
--white: #f7f7f7;
--surface: rgba(255, 255, 255, 0.28);
--font-sans: Inter, -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-serif: "Source Serif", "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif;
--column-width: min(72rem, calc(100% - 3rem));
--content-width: min(48rem, calc(100% - 3rem));
}
* {
box-sizing: border-box;
}
html {
font-size: 14px;
background: var(--bg);
}
a {
color: inherit;
font-weight: 500;
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 0.2rem;
&:hover {
color: var(--hover);
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 0.2rem;
}
}
.icon {
align-items: center;
display: inline-flex;
flex: 0 0 auto;
height: 1em;
justify-content: center;
line-height: 1;
width: 1em;
}
.icon svg {
display: block;
height: 100%;
width: 100%;
}
h1 {
font-size: clamp(2.5rem, 10dvw, 4rem);
font-weight: 700;
letter-spacing: -0.5px;
line-height: 1;
margin: 0 0 1rem;
}
h2 {
font-size: clamp(1.5rem, 4vw, 2rem);
font-weight: 700;
line-height: 1.3;
margin: 0 0 1.5rem;
}
h3 {
color: var(--muted);
font-size: 1.4rem;
font-weight: 700;
line-height: 1.3;
}
body {
margin: 0 auto;
padding: 0;
min-height: 100dvh;
background: var(--bg);
color: var(--text);
display: grid;
font-family: var(--font-sans);
grid-template-rows: 5rem 1fr auto;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
width: 100dvw;
/* site header and footer defaults */
> header,
> footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
/* site header */
> header {
border-bottom: 1px solid var(--light);
padding: 0 1.5rem;
.title {
align-items: center;
display: flex;
gap: 1rem;
height: 100%;
align-items: stretch;
}
.logo,
.page {
align-items: center;
display: flex;
font-size: 1.5rem;
gap: 0.5rem;
text-decoration: none;
}
.logo {
font-weight: 700;
}
.page {
font-weight: 700;
}
.logo:has(+ .page) {
color: var(--faded);
}
.scheme {
color: var(--faded);
}
.param {
color: var(--text);
}
nav {
align-items: center;
display: none;
gap: 1.5rem;
font-size: 1rem;
a {
text-decoration: none;
}
}
}
/* site footer */
> footer {
color: var(--muted);
display: flex;
flex-direction: column;
gap: 2rem;
padding-top: 2rem;
section {
width: var(--column-width);
}
#footnotes ul {
list-style: none;
margin: 0;
padding: 0;
li {
display: grid;
grid-template-columns: 1.5rem auto;
span.symbol {
font-size: 1.25rem;
}
}
}
}
}
/* case study wrapper */
main {
display: grid;
justify-items: center;
/* layout handling for the top sections */
header,
aside {
display: flex;
width: var(--column-width);
}
/* top hero */
header {
padding: 0;
/* temporary height clamping */
&:has(+ aside) {
display: grid;
align-content: end;
min-height: clamp(12rem, 50dvh, 18rem);
}
.lede {
margin: 1rem 0 0;
font-size: 1.6rem;
line-height: 1.4;
}
}
/* stats */
aside {
align-items: flex-start;
display: flex;
flex-direction: column;
gap: 1.5rem;
justify-content: flex-start;
margin-top: 3rem;
figure {
display: grid;
grid-template-columns: 2.25rem auto;
grid-template-rows: 1.5rem auto;
margin: 0;
--icon: var(--blue);
span.icon {
width: 1.5rem;
height: 1.5rem;
color: var(--icon);
}
label {
color: var(--text);
font-size: 1rem;
text-transform: uppercase;
font-weight: 600;
}
figcaption {
color: var(--muted);
font-size: 1.05rem;
line-height: 1.4;
grid-column-start: 2;
}
&:nth-child(2) {
--icon: var(--orange);
}
&:nth-child(3) {
--icon: var(--green);
}
&:nth-child(4) {
--icon: var(--pink);
}
&:nth-child(5) {
--icon: var(--cyan);
}
}
}
}
/* case study content */
article {
background: var(--white);
display: grid;
gap: 2rem;
grid-template-columns: 1fr;
justify-content: center;
padding: 4rem 1.5rem 3rem;
width: 100%;
nav#toc {
align-self: start;
position: sticky;
margin-left: -1.5rem;
top: 0;
width: calc(100% + 3rem);
ol {
border-top: 3px solid var(--light);
display: grid;
list-style: decimal-leading-zero;
list-style-position: inside;
margin: 0;
padding: 0;
li {
background: var(--white);
border-bottom: 1px solid var(--light);
height: 2.5rem;
padding: 0.4rem 0.25rem 0;
&::marker {
color: var(--muted);
}
&:last-child {
border: none;
}
}
}
a {
color: var(--muted);
text-decoration: none;
transition: padding 125ms ease;
&.active {
color: var(--text);
font-weight: 700;
padding-left: 0.25rem;
}
&:hover {
color: var(--text);
}
}
}
#content {
display: grid;
gap: 1.5rem;
}
section {
width: 100%;
&#intro {
p:first-of-type {
font-size: 1.1rem;
}
}
}
figure {
/*background: var(--bg);*/
align-items: center;
display: flex;
flex-direction: column;
gap: 2rem;
justify-content: center;
margin: 2rem 0;
padding: 0;
position: relative;
img {
max-width: 100%;
&.desktop {
aspect-ratio: 1.4 / 1;
}
&.mobile {
aspect-ratio: 0.46 / 1;
height: 20rem;
}
}
figcaption {
color: var(--muted);
font-size: 0.875rem;
line-height: 1.5;
}
}
}
/**
* display queries */
.tablet-show,
.laptop-show,
.display-show {
display: none;
}
/* tablets */
@media (min-width: 640px)
{
html {
font-size: 16px;
}
body > header {
padding: 0 2rem;
nav {
display: flex;
}
}
main {
header {
.lede {
max-width: 60%;
}
}
aside {
flex-direction: row;
gap: 2rem;
figure {
grid-template-columns: auto;
grid-template-rows: 2rem 2rem auto;
max-width: 20%;
padding-right: 1rem;
figcaption {
grid-column-start: 1;
}
}
}
}
.tablet-show {
display: block;
}
.tablet-hide {
display: none;
}
}
/* laptops */
@media (min-width: 1024px)
{
article {
gap: 3rem;
grid-template-columns: 12rem var(--content-width);
nav#toc {
margin-left: 0;
top: 4rem;
width: 12rem;
ol {
margin-top: 0.75rem;
}
}
}
}
/* desktops */
@media (min-width: 1280px)
{
html {
font-size: 17px;
}
article {
gap: 0;
grid-template-columns: 16rem var(--content-width) 8rem;
}
}
/* displays */
@media (min-width: 1440px)
{
html {
font-size: 18px;
}
article {
padding: 4rem 0 3rem;
nav#toc {
font-size: 0.925rem;
ol li {
padding-top: 0.5rem;
}
}
figure {
align-items: flex-end;
flex-direction: row;
figcaption {
font-size: 0.75rem;
position: absolute;
right: -10rem;
bottom: 1rem;
width: 8rem;
}
}
}
}
/**
* deprecate? */
@media (max-width: 900px) {
article #content {
width: 100%;
}
article section {
width: 100%;
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12h.01"/><path d="M16 6V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/><path d="M22 13a18.15 18.15 0 0 1-20 0"/><rect width="20" height="14" x="2" y="6" rx="2"/></svg>

After

Width:  |  Height:  |  Size: 351 B

10
assets/icons/calendar.svg Normal file
View File

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="18" height="18" x="3" y="4" rx="2"/>
<path d="M16 2v4"/>
<path d="M3 10h18"/>
<path d="M8 2v4"/>
<path d="M17 14h-6"/>
<path d="M13 18H7"/>
<path d="M7 14h.01"/>
<path d="M17 18h.01"/>
</svg>

After

Width:  |  Height:  |  Size: 419 B

1
assets/icons/hat.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22.051,15.585c-3.091,-0.509 -3.239,0.05 -5.917,-0.211c-2.885,-0.282 -4.783,-1.665 -10.049,-1.747c0.273,-5.654 3.125,-8.601 9.172,-8.849c5.429,1.172 8.503,4.785 6.794,10.808Zm-11.997,-1.74c0.209,-3.688 1.521,-6.855 5.295,-9.068c2.519,1.617 2.923,5.219 1.438,10.595l1.846,0.213c-2.102,2.632 -5.155,4.143 -7.918,3.482c-2.596,-0.621 -3.795,-3.087 -9.24,-2.071l4.61,-3.369" /></svg>

After

Width:  |  Height:  |  Size: 568 B

1
assets/icons/pen.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15.707 21.293a1 1 0 0 1-1.414 0l-1.586-1.586a1 1 0 0 1 0-1.414l5.586-5.586a1 1 0 0 1 1.414 0l1.586 1.586a1 1 0 0 1 0 1.414z"/><path d="m18 13-1.375-6.874a1 1 0 0 0-.746-.776L3.235 2.028a1 1 0 0 0-1.207 1.207L5.35 15.879a1 1 0 0 0 .776.746L13 18"/><path d="m2.3 2.3 7.286 7.286"/><circle cx="11" cy="11" r="2"/></svg>

After

Width:  |  Height:  |  Size: 507 B

1
assets/icons/users.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><path d="M16 3.128a4 4 0 0 1 0 7.744"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><circle cx="9" cy="7" r="4"/></svg>

After

Width:  |  Height:  |  Size: 346 B

5
content/_index.md Normal file
View File

@ -0,0 +1,5 @@
---
title: "Portfolio"
---
Product case studies and selected work.

View File

@ -0,0 +1,5 @@
---
title: "Case Studies"
---
Selected product design and UX case studies.

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

View File

@ -0,0 +1,138 @@
---
title: "Legacy app refresh"
layout: "study"
slug: "legacy"
summary: "Reshaping a complex and outdated school forms workflow into a clearer, calmer experience for administrators and families."
draft: false
hero:
title: "Legacy app refresh"
deck: "Reshaping a critical but outdated school forms workflow into a calmer, clearer experience for administrators and families."
tags:
- label: "Figma"
tone: "green"
- label: "Product Design"
tone: "yellow"
- label: "B2B SaaS"
tone: "cyan"
- label: "Workflow UX"
tone: "blue"
study:
facts:
- label: "Role"
value: "Product Design"
icon: "hat"
- label: "Timeline"
value: "Est. 6 months, expanded to 18 months"
icon: "calendar"
- label: "Tools"
value: "Figma"
icon: "pen"
- label: "Industry"
value: "K-12 education SaaS"
icon: "briefcase"
- label: "Stakeholders"
value: "Product, engineering, and school operations"
icon: "users"
toc:
- id: "intro"
label: "Introduction"
- id: "overview"
label: "Project overview"
- id: "problems"
label: "Core problems"
- id: "context"
label: "Delivery context"
- id: "response"
label: "Response"
- id: "impact"
label: "Impact"
- id: "reflection"
label: "Reflection"
footnotes:
- symbol: "&dagger;"
note: "Product names have been changed to maintain confidentiality. I'm happy to answer product questions as needed, but would need permission to share specifics like names and customers."
---
{{< intro id="intro" title="Designing for clarity over polish in complex admin software" >}}
Wallabyte&dagger; supports schools and families through critical paperwork&mdash;from permissions to registrations and even student health information. It solved this and more, but was **woefully unusable, even by admin-ware standards**. The forms experience had become frustrating enough that major customers, like NYC, were at risk of churn unless the workflow became **clearer, calmer, and easier to use**.
This case study covers how I approached that challenge&mdash;from identifying the highest impact issues to shaping a more coherent forms experience&mdash;all within the paradox of a userbase simultaneously frustrated yet resistant to change.
It is also meant to show how I simplify complex systems; work in a difficult set of circumstances; and **design to please the user, not myself**.
{{< /intro >}}
{{< overview
id="overview"
title="Project overview"
>}}
Wallabyte had two halves: an administrative app for creating and managing forms, and a parent- and staff-facing app for completing forms. This redesign only touched this second, smaller part&mdash;called **Wallabyte Central**&mdash;due to the particular churn risk it was creating.
The existing experience had become painful enough that major customers, including NYC, were at risk of leaving unless the product became significantly easier to use. My job was to figure out how to make their mobile and desktop experiences more efficient and pleasant.
At the same time, Wallabyte was also rebranding as **Schooltastic Forms**, now a year after it had been acquired by Schooltastic. This project was my first interaction with this app and team, and not only did I have to get up to speed quickly, its users could be seeing a lot of sudden change.
<figure>
<img src="img/overview.png" />
<figcaption>Original app screens for Wallabyte Central on desktop and mobile.</figcaption>
</figure>
{{< /overview >}}
{{< problems id="problems" title="Core problems" >}}
We identified four main UI/UX issues to target in this refresh, following my own deepdive into the product and several meetings with the product manager.
I was fortunate to share an entirely fresh set of eyes on a mature product, and it helped question a lot of assumptions that had been taken for granted over the yers. This app necessitated multiple demo accounts with different permissions and scopes, and I spent the first week cataloguing my experiences trying to perform a number of tasks from the PM.
These four problems became the core problem statements we used to frame the work and evaluate whether the redesign was actually solving the right things. The PM initially estimated a six-month path from early design through implementation.
{{< problem
title="Forms were too hard to find and track"
image="img/overview.png"
alt="Legacy interface showing forms split across different lists and patterns"
caption="Representative legacy flow showing how requested, in-progress, and completed forms were fragmented across the experience."
>}}
This was perhaps the chief user complaint and the most immediately obvious usability issue. The app distinguished forms that were _requested_ by the school to be completed, versus those _initiated_ by the end user from a collection of available forms.
Users had to remember how a form had been started just to find it again, and unfinished work was treated differently in both sections. **These distinctions didn't matter to the user** who just wanted to get in and get out.
{{< /problem >}}
{{< problem
title="The app was designed for desktop, but used primarily on mobile"
image="img/overview.png"
alt="Legacy interface showing desktop-oriented structure despite mobile-heavy usage"
caption="Representative legacy flow showing structure and density that translated poorly to mobile use."
>}}
Despite 94% of end users completing forms on mobile devices, the app was never thought through from a mobile perspective.
On mobile, critical actions were buried below static information or long lists, pages were never optimized and often zoomed out, 2-axis scrolling was prevalent on data-dense screens, and staff members with sometimes hundreds of forms never saw critical alerts.
{{< /problem >}}
{{< problem
title="Navigation was complex"
image="img/overview-mobile.png"
alt="Legacy mobile flow showing navigation and terminology that did not match user mental models"
caption="Representative mobile flow showing navigation and task structure shaped more by product logic than user intent."
>}}
Users needed to answer simple questions like “What needs my attention?” and “Where do I go next?”, but the navigation, terminology, and underlying data model reflected internal product logic instead of user mental models.
{{< /problem >}}
{{< problem
title="Managing forms across multiple children, schools, or contexts was confusing"
image="img/overview-desktop.png"
alt="Legacy desktop interface showing form management across multiple children or school contexts"
caption="Representative desktop screen showing how scope and context could become unclear across children, schools, or approval states."
>}}
Parents and staff often needed to act on behalf of multiple children, schools, or approval and reporting contexts, but the interface did not make that scope clear enough. Users could easily lose track of whose information they were viewing or updating.
{{< /problem >}}
{{< /problems >}}

21
hugo.yaml Normal file
View File

@ -0,0 +1,21 @@
baseURL: "https://portfolio.lan"
languageCode: "en-us"
title: "Andrew Gioia | Case Studies"
enableRobotsTXT: true
disableKinds:
- taxonomy
- term
- RSS
params:
description: "Portfolio and product case studies by Andrew Gioia."
permalinks:
studies: "/studies/:slug/"
taxonomies: {}
markup:
goldmark:
renderer:
unsafe: true

View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{ $description := .Site.Params.description }}
{{ with .Description }}
{{ $description = . }}
{{ else }}
{{ with .Summary }}
{{ $description = . | plainify }}
{{ end }}
{{ end }}
<title>
{{ if .IsHome }}
{{ .Site.Title }}
{{ else }}
{{ .Title }} | {{ .Site.Title }}
{{ end }}
</title>
<meta name="description" content="{{ $description }}">
{{ with resources.Get "css/site.css" }}
<link rel="stylesheet" href="{{ .RelPermalink }}">
{{ end }}
{{ if and .IsPage (eq .Section "studies") .Params.study.toc }}
<script src="{{ "js/main.js" | relURL }}" defer></script>
{{ end }}
</head>
<body>
<header>
<div class="title">
<a class="logo" href="{{ "/" | relURL }}">
<span class="tablet-hide">AG</span>
<span class="tablet-show">Andrew Gioia</span>
</a>
{{- if and .IsPage (eq .Section "studies") -}}
<a class="page" href="{{ "/studies" | relURL }}">
Case study
</a>
{{ end }}
</div>
<nav aria-label="Primary">
<a href="{{ "/studies/" | relURL }}">Case studies</a>
<a href="{{ "/contact/" | relURL }}">Get in touch</a>
</nav>
</header>
<main>
{{ block "main" . }}{{ end }}
</main>
<footer>
{{ partial "footer.html" . }}
</footer>
</body>
</html>

View File

@ -0,0 +1,23 @@
{{ define "main" }}
<section class="page-hero page-hero-compact">
<p class="eyebrow">{{ .Section }}</p>
<h1>{{ .Title }}</h1>
{{ with .Content }}
<div class="lede">{{ . }}</div>
{{ end }}
</section>
<section class="page-section">
<div class="study-list">
{{ range .Pages.ByWeight }}
<article>
<p class="study-card-meta">{{ .Section | title }}</p>
<h2><a href="{{ .RelPermalink }}">{{ .Title }}</a></h2>
<p>{{ .Summary }}</p>
</article>
{{ else }}
<p>No entries yet.</p>
{{ end }}
</div>
</section>
{{ end }}

View File

@ -0,0 +1,14 @@
{{ define "main" }}
<article>
<header>
<p class="eyebrow">{{ .Section | title }}</p>
<h1>{{ .Title }}</h1>
{{ with .Summary }}
<p class="lede">{{ . }}</p>
{{ end }}
</header>
<section class="page-section rich-copy">
{{ .Content }}
</section>
</article>
{{ end }}

View File

@ -0,0 +1,54 @@
{{- define "main" -}}
<header>
{{- with .Params.hero.tags -}}
<!--<ul class="tag-list" aria-label="Case study tags">
{{- range . -}}
<li>
<span class="tag{{ with .tone }} tag-{{ . }}{{ end }}">{{ .label }}</span>
</li>
{{- end -}}
</ul>-->
{{- end -}}
<h1>{{ with .Params.hero.title }}{{ . }}{{ else }}{{ .Title }}{{ end }}</h1>
{{- with .Params.hero.deck -}}
<p class="lede">{{ . }}</p>
{{- end -}}
{{- if and (not .Params.hero.deck) .Summary -}}
<p class="lede">{{ .Summary }}</p>
{{- end -}}
</header>
{{- with .Params.study -}}
<aside aria-label="Study metadata">
{{- with .facts -}}
{{- range . -}}
<figure>
{{- with .icon -}}
{{ partial "icon.html" (dict "name" .) }}
{{- end -}}
<label>{{ .label }}</label>
<figcaption>{{ .value }}</figcaption>
</figure>
{{- end -}}
{{- end -}}
</aside>
{{- end -}}
<div style="width: 100%; height: 50dvh; border-bottom: 2px solid #171717;"></div>
<article>
{{- with .Params.study.toc -}}
<nav id="toc" aria-label="Table of contents">
<ol>
{{- range . -}}
<li><a href="#{{ .id }}">{{ .label }}</a></li>
{{- end -}}
</ol>
</nav>
{{- end -}}
<div id="content">
{{ .Content }}
</div>
</article>
{{- end -}}

25
layouts/index.html Normal file
View File

@ -0,0 +1,25 @@
{{ define "main" }}
<section class="page-hero">
<p class="eyebrow">Portfolio</p>
<h1>Case studies with room for the full story.</h1>
<p class="lede">
A custom Hugo setup for showcasing product and UX work without relying on a theme.
</p>
</section>
<section class="page-section">
<div class="section-heading">
<p class="eyebrow">Selected work</p>
<h2>Current case studies</h2>
</div>
<div class="study-list">
{{ range where .Site.RegularPages "Section" "studies" }}
<article>
<p class="study-card-meta">{{ with .Params.hero.title }}{{ . }}{{ else }}{{ .Title }}{{ end }}</p>
<h3><a href="{{ .RelPermalink }}">{{ .Title }}</a></h3>
<p>{{ .Summary }}</p>
</article>
{{ end }}
</div>
</section>
{{ end }}

View File

@ -0,0 +1,29 @@
{{- $footnotes := slice -}}
{{- with .Params.footnotes -}}
{{- $footnotes = . -}}
{{- else -}}
{{- with .Params.study.footnotes -}}
{{- $footnotes = . -}}
{{- end -}}
{{- end -}}
{{- if gt (len $footnotes) 0 -}}
<section id="footnotes">
<ul aria-label="Footnotes">
{{- range $footnotes -}}
<li class="footnote">
{{- with .symbol -}}
<span class="symbol">{{ . | safeHTML }}</span>
{{- end -}}
{{- with .note -}}
<span class="note">{{ . }}</span>
{{- end -}}
</li>
{{- end -}}
</ul>
</section>
{{- end -}}
<section id="end">
<p>Footer.</p>
</section>

View File

@ -0,0 +1,11 @@
{{- $name := .name | default . -}}
{{- $class := .class -}}
{{- $label := .label -}}
{{- $path := printf "icons/%s.svg" $name -}}
{{- with resources.Get $path -}}
<span class="icon icon-{{ $name }}{{ with $class }} {{ . }}{{ end }}"{{ if $label }} role="img" aria-label="{{ $label }}"{{ else }} aria-hidden="true"{{ end }}>
{{ .Content | safeHTML }}
</span>
{{- else -}}
{{- errorf "Icon not found: %s" $path -}}
{{- end -}}

View File

@ -0,0 +1,4 @@
{{- $name := .Get "name" | default (.Get 0) -}}
{{- $class := .Get "class" -}}
{{- $label := .Get "label" -}}
{{- partial "icon.html" (dict "name" $name "class" $class "label" $label) -}}

View File

@ -0,0 +1,8 @@
{{- $id := .Get "id" | default "intro" -}}
{{- $title := .Get "title" -}}
<section id="{{ $id }}">
{{- with $title }}
<h2>{{ . }}</h2>
{{- end }}
{{ .Inner | markdownify }}
</section>

View File

@ -0,0 +1,14 @@
{{- $id := .Get "id" | default "overview" -}}
{{- $title := .Get "title" | default "Project overview" -}}
{{- $businessReasonTitle := .Get "business_reason_title" | default "Business reason for the work" -}}
{{- $mediaCaption := .Get "media_caption" -}}
{{- $mediaPlaceholder := .Get "media_placeholder" | default "Original product screenshot placeholder" -}}
<section id="{{ $id }}">
<h2>{{ $title }}</h2>
{{- with .Inner }}
{{ . | markdownify }}
{{- end }}
</section>

View File

@ -0,0 +1,32 @@
{{- $title := .Get "title" -}}
{{- $image := .Get "image" -}}
{{- $alt := .Get "alt" | default $title -}}
{{- $caption := .Get "caption" -}}
{{- $placeholder := .Get "placeholder" | default "Problem screenshot placeholder" -}}
{{- with $title }}
<h3>{{ . }}</h3>
{{- end }}
{{ .Inner | markdownify }}
<!--
<figure class="problem-figure">
{{- if $image -}}
{{- with $.Page.Resources.GetMatch $image -}}
<img src="{{ .RelPermalink }}" alt="{{ $alt }}">
{{- else -}}
<div class="problem-image-placeholder">
<p>{{ $placeholder }}</p>
</div>
{{- end -}}
{{- else -}}
<div class="problem-image-placeholder">
<p>{{ $placeholder }}</p>
</div>
{{- end }}
{{- with $caption }}
<figcaption>{{ . }}</figcaption>
{{- end }}
</figure>
-->

View File

@ -0,0 +1,8 @@
{{- $id := .Get "id" | default "problems" -}}
{{- $title := .Get "title" | default "Core problems" -}}
<section id="{{ $id }}">
<h2>{{ $title }}</h2>
<div class="problems">
{{ .Inner | markdownify }}
</div>
</section>

8
package.json Normal file
View File

@ -0,0 +1,8 @@
{
"name": "portfolio",
"private": true,
"scripts": {
"build": "hugo",
"watch": "hugo server -D --disableFastRender"
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

44
static/js/main.js Normal file
View File

@ -0,0 +1,44 @@
document.addEventListener("DOMContentLoaded", () => {
const toc = document.querySelector("article > nav#toc");
const content = document.querySelector("article > div#content");
if (!toc || !content) {
return;
}
const links = Array.from(toc.querySelectorAll('a[href^="#"]'));
const sections = Array.from(content.querySelectorAll("section[id]"));
if (!links.length || !sections.length) {
return;
}
const linksById = new Map(
links.map((link) => [decodeURIComponent(link.getAttribute("href").slice(1)), link]),
);
const setActive = (id) => {
links.forEach((link) => {
link.classList.toggle("active", linksById.get(id) === link);
});
};
const updateActive = () => {
const threshold = window.innerHeight * 0.25;
let current = sections[0];
sections.forEach((section) => {
if (section.getBoundingClientRect().top <= threshold) {
current = section;
}
});
if (current?.id) {
setActive(current.id);
}
};
updateActive();
window.addEventListener("scroll", updateActive, { passive: true });
window.addEventListener("resize", updateActive);
});