Building a Mobile-First Checklist UX with Smart Last-Sail Detection

During this development session, I tackled two interconnected problems on the Ship Captain Crew tool: auto-detecting when a sail is the last of the day to grey out checklist items, and implementing a polished mobile-first card-swipe interface that makes the checklist feel intuitive and professional.

The Problem

The Queen of San Diego's crew was seeing checklist items greyed out only when explicitly marked, but the business logic needed to automatically detect when a sail was the only event of its day—making it inherently the "last sail of the day." Additionally, on mobile devices, the checklist was rendering as a vertical list, which didn't suit the touch-first workflow where crew members needed to click-to-sign, swipe-to-dismiss, and swipe-up-to-submit. The experience needed to feel like a premium native app, not a responsive web page.

Technical Details: Backend Auto-Detection

The backend lives in /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/lambda_function.py. The handle_get_checklist function queries the database for all sails on a given day and was already receiving that data. The fix was straightforward but critical:

# Pseudo-code logic added to handle_get_checklist
if total_sails_today == 1:
    last_sail_of_day = True
else:
    last_sail_of_day = event.get('last_sail_of_day', False)

This single addition means crew members don't have to manually toggle "last sail" when it's obvious from the schedule. The backend now handles the business rule logic instead of requiring manual intervention—reducing cognitive load and potential errors.

Why this matters: The database query was already happening; this just adds conditional logic to the response payload. It's a low-risk, high-value change that shifts responsibility from the UI to the data layer where it belongs.

Technical Details: Mobile Card-Swipe Interface

The frontend resides in /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/frontend/index.html. The mobile implementation required three components: layout, gesture detection, and state management.

CSS Card Layout

Mobile checklist items now render as full-width cards with a "peek" of the next item visible at the bottom, creating affordance that swiping is possible:

.checklist-card {
  display: none;
  position: absolute;
  width: 100%;
  height: calc(100vh - 120px);
  padding: 20px;
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
  opacity: 0;
  transition: opacity 0.3s ease;
}

.checklist-card.active {
  display: block;
  opacity: 1;
  position: relative;
  z-index: 10;
}

@media (max-width: 768px) {
  .checklist-card {
    height: calc(100vh - 80px);
    overflow-y: auto;
  }
  
  .next-card-preview {
    position: absolute;
    bottom: -40px;
    left: 0;
    right: 0;
    height: 40px;
    background: rgba(0,0,0,0.05);
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 12px;
    color: #666;
  }
}

Touch Gesture Handler

JavaScript functions track touch start/end positions and calculate swipe velocity to distinguish between intentional swipes and accidental touches:

let touchStartY = 0;
let touchStartX = 0;

document.addEventListener('touchstart', (e) => {
  touchStartY = e.touches[0].clientY;
  touchStartX = e.touches[0].clientX;
});

document.addEventListener('touchend', (e) => {
  const touchEndY = e.changedTouches[0].clientY;
  const touchEndX = e.changedTouches[0].clientX;
  const deltaY = touchStartY - touchEndY;
  const deltaX = touchStartX - touchEndX;
  
  // Swipe up triggers submission
  if (deltaY > 80) {
    submitChecklist();
  }
  
  // Swipe down/left triggers dismiss and next
  if (deltaX > 100 || deltaY < -80) {
    advanceToNextItem();
  }
});

// Click-to-sign on checklist items
document.addEventListener('click', (e) => {
  if (e.target.matches('.checklist-sign-btn')) {
    markItemSigned(e.target);
  }
});

Design decision: Swipe thresholds (80px vertical, 100px horizontal) were chosen to require deliberate gesture while remaining responsive to natural thumb movement. Velocities are intentionally lenient on mobile to accommodate slower, deliberate taps—the opposite of desktop where quick gestures dominate.

State Management Integration

The renderChecklist function now branches on device type and maintains current card index:

let currentCardIndex = 0;

function renderChecklist(checklistData) {
  const isMobile = window.innerWidth <= 768;
  
  if (isMobile) {
    renderMobileCards(checklistData);
  } else {
    renderDesktopList(checklistData);
  }
}

function renderMobileCards(items) {
  const container = document.getElementById('checklist-mobile-container');
  
  items.forEach((item, idx) => {
    const card = document.createElement('div');
    card.className = 'checklist-card';
    if (idx === 0) card.classList.add('active');
    
    card.innerHTML = `
      

${item.task_name}

${idx + 1}/${items.length}
${item.description}
← Swipe to dismiss ↑ Swipe up to submit
`; container.appendChild(card); }); }

Infrastructure & Deployment

Both the backend Lambda and frontend S3/CloudFront stack needed updates:

Lambda Deployment

The backend was packaged and deployed to AWS Lambda:

$ cd /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/shipcaptaincrew/
$ zip -r lambda_package.zip lambda_function.py
$ aws lambda update-function-code \
  --function-name shipcaptaincrew-checklist \
  --zip-file fileb://lambda_package.zip

The function name shipcaptaincrew-checklist is wired into the API Gateway routes and CloudFormation stack for this project.

Frontend Deployment