Initial content sections for forms case study; improvements to the layout at all breakpoints and landing on a better design
14
README.md
@ -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
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "{{ replace .File.ContentBaseName `-` ` ` | title }}"
|
||||
date: {{ .Date }}
|
||||
draft: true
|
||||
---
|
||||
513
assets/css/site.css
Normal 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%;
|
||||
}
|
||||
}
|
||||
1
assets/icons/briefcase.svg
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Portfolio"
|
||||
---
|
||||
|
||||
Product case studies and selected work.
|
||||
5
content/studies/_index.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Case Studies"
|
||||
---
|
||||
|
||||
Selected product design and UX case studies.
|
||||
BIN
content/studies/legacy/img/overview-desktop.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
content/studies/legacy/img/overview-mobile.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
content/studies/legacy/img/overview.png
Normal file
|
After Width: | Height: | Size: 260 KiB |
138
content/studies/legacy/index.md
Normal 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: "†"
|
||||
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† supports schools and families through critical paperwork—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—from identifying the highest impact issues to shaping a more coherent forms experience—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—called **Wallabyte Central**—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
@ -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
|
||||
54
layouts/_default/baseof.html
Normal 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>
|
||||
23
layouts/_default/list.html
Normal 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 }}
|
||||
14
layouts/_default/single.html
Normal 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 }}
|
||||
54
layouts/_default/study.html
Normal 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
@ -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 }}
|
||||
29
layouts/partials/footer.html
Normal 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>
|
||||
11
layouts/partials/icon.html
Normal 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 -}}
|
||||
4
layouts/shortcodes/icon.html
Normal 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) -}}
|
||||
8
layouts/shortcodes/intro.html
Normal file
@ -0,0 +1,8 @@
|
||||
{{- $id := .Get "id" | default "intro" -}}
|
||||
{{- $title := .Get "title" -}}
|
||||
<section id="{{ $id }}">
|
||||
{{- with $title }}
|
||||
<h2>{{ . }}</h2>
|
||||
{{- end }}
|
||||
{{ .Inner | markdownify }}
|
||||
</section>
|
||||
14
layouts/shortcodes/overview.html
Normal 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>
|
||||
32
layouts/shortcodes/problem.html
Normal 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>
|
||||
-->
|
||||
8
layouts/shortcodes/problems.html
Normal 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
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "portfolio",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "hugo",
|
||||
"watch": "hugo server -D --disableFastRender"
|
||||
}
|
||||
}
|
||||
BIN
static/fonts/agrandir-bold.otf
Normal file
BIN
static/fonts/source-serif/italic.ttf.woff2
Normal file
BIN
static/fonts/source-serif/italic.woff
Normal file
BIN
static/fonts/source-serif/roman.woff
Normal file
BIN
static/fonts/source-serif/roman.woff2
Normal file
44
static/js/main.js
Normal 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);
|
||||
});
|
||||