Skip to main content

Version 3: UI/UX & Security

Focus: Design System, Price Validation, Loading States


Overview

Version 3 establishes a consistent design system and fixes the critical price manipulation vulnerability from v2.

Goal: Polish UI + Eliminate Security Vulnerabilities

3A - Design System

Objective

Create a consistent, polished visual design language.

CSS Variables

File: src/app/globals.css

:root {
/* Colors */
--color-primary: #007aff;
--color-primary-hover: #0051d5;

--color-text-primary: #1a1a1a;
--color-text-secondary: #666666;
--color-text-tertiary: #999999;

--color-background: #fafafa;
--color-surface: #ffffff;
--color-border: #e5e5e5;

--color-error: #ff3b30;
--color-success: #34c759;

/* Radius */
--radius-md: 12px;
--radius-full: 9999px;
}

Typography Scale

LevelClassUsage
Page Titletext-2xl md:text-3xl font-semiboldProduct name, headers
Sectiontext-lg font-semiboldSection headings
Bodytext-sm md:text-baseDescriptions
Smalltext-xsHelper text
Labeltext-[11px] uppercase tracking-[0.16em]Category labels

Component Patterns

Primary Button:

className="bg-black text-white hover:bg-gray-900
rounded-full px-6 py-2.5 text-sm font-medium
transition-colors active:scale-95"

Secondary Button:

className="border border-[var(--color-border)] bg-white
text-[var(--color-text-secondary)] hover:border-gray-400
rounded-full px-3 py-1.5 h-9 text-xs font-medium"

Card:

className="rounded-2xl bg-white border border-[var(--color-border)]
shadow-sm hover:shadow-lg transition-all duration-300"

Animation System

@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
SpeedDurationUse Case
Fast150msMicro-interactions
Base200msStandard transitions
Slow300-500msPage loads, images

Design Inspirations

SourceElement
AppleColor palette, subtle shadows
Verve CoffeeTypography, spacing
Pure CyclesMinimalist cards

3B - Price Validation

Objective

Eliminate price manipulation vulnerability.

The Problem

// ❌ v2: Frontend sends price (VULNERABLE)
const { items } = await req.json();
// items = [{ id: 1, price: 0.01, quantity: 100 }] ← Attacker modified!

const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
// total = $1.00 instead of $2,999.00

The Fix

// ✅ v3: Backend fetches price from database
export async function POST(req: Request) {
const { items } = await req.json();

// Only trust productId and quantity from frontend
const productIds = items.map((item) => item.productId);

// Fetch REAL prices from database
const result = await query(
"SELECT id, price, name FROM products WHERE id = ANY($1)",
[productIds]
);

const productMap = new Map(result.rows.map((r) => [r.id, r]));

// Calculate total using DATABASE prices
const total = items.reduce((sum, item) => {
const product = productMap.get(item.productId);
return sum + Number(product.price) * item.quantity;
}, 0);

// ... create Stripe session with database prices
}

Additional Validations

Quantity Limits:

for (const item of items) {
if (
!Number.isInteger(item.quantity) ||
item.quantity < 1 ||
item.quantity > 100
) {
return NextResponse.json({ error: "Invalid quantity" }, { status: 400 });
}
}

Product Existence:

const foundIds = result.rows.map((r) => r.id);
const missingIds = productIds.filter((id) => !foundIds.includes(id));

if (missingIds.length > 0) {
return NextResponse.json(
{ error: `Products not found: ${missingIds.join(", ")}` },
{ status: 400 }
);
}

3C - Loading States

Objective

Polish user experience during async operations.

Skeleton Loading

.skeleton {
background: linear-gradient(
90deg,
var(--color-border) 0%,
var(--color-surface) 50%,
var(--color-border) 100%
);
background-size: 200% 100%;
animation: shimmer 2s infinite linear;
}

@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}

Product Grid Skeleton

{
loading && (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="animate-pulse">
<div className="aspect-[4/5] rounded-2xl bg-gray-200" />
<div className="mt-3 h-4 w-3/4 rounded bg-gray-200" />
<div className="mt-2 h-4 w-1/2 rounded bg-gray-200" />
</div>
))}
</div>
);
}

Empty States

{
!loading && products.length === 0 && (
<div className="text-center py-16">
<p className="text-lg font-medium mb-2">No products found</p>
<p className="text-sm text-gray-500 mb-4">Try adjusting your filters</p>
<button className="bg-black text-white rounded-full px-6 py-2.5">
View All Products
</button>
</div>
);
}

3D - Responsive Design

Breakpoints

sm:  640px   → Mobile landscape
md: 768px → Tablet
lg: 1024px → Laptop
xl: 1280px → Desktop

Product Grid

// 2 columns mobile → 4 columns desktop
className = "grid grid-cols-2 gap-4 md:gap-8 lg:grid-cols-3 xl:grid-cols-4";

Product Card Adjustments

ElementMobileDesktop
Border radiusrounded-xlrounded-2xl
Gapgap-4gap-8
Badge texttext-[10px]text-xs
Marginmt-2mt-3

3E - Accessibility

Focus States

*:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
border-radius: 2px;
}

Reduced Motion

@media (prefers-reduced-motion: reduce) {
*,
::before,
::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}

Semantic HTML

// Use semantic elements
<main>
<section>
<article> {/* Product card */}
<h2>{product.name}</h2>
</article>
</section>
</main>

// Use aria labels
<button aria-label="Add to cart">
<ShoppingCart />
</button>

Security Status After v3

Vulnerabilityv2 Statusv3 Status
Price Manipulation⚠️ Vulnerable✅ Fixed
SQL Injection✅ Safe✅ Safe
Quantity Abuse⚠️ No limit✅ 1-100
Invalid Products⚠️ No check✅ Verified

Files Modified

FileChanges
src/app/globals.cssDesign tokens, animations
src/app/api/checkout/route.tsPrice validation
src/app/products/page.tsxSkeleton loading
src/app/products/[id]/page.tsxLoading states
src/app/components/ProductCard.tsxNew design

Design Decisions

Why CSS Variables?

/* Easy to update, consistent across app */
background: var(--color-background);

/* vs. hardcoded */
background: #fafafa; /* What if we need dark mode? */

Why Tailwind?

  • Utility-first: Fast iteration
  • Responsive: Built-in breakpoints
  • Consistent: Design tokens via config
  • No naming: No .product-card-container-wrapper

Why Not Component Library?

  • Learning: Build from scratch to understand
  • Customization: Full control over design
  • Bundle size: Only what we need

Next Version Preview

Version 4.0 will add user authentication with NextAuth and Redis caching for performance. This marks the MVP milestone.