Skip to Content

Projects

Parent Center

Co-led the technical build of ABCmouse Parent Center — a SvelteKit parent hub for child progress, profiles, and account settings that runs on the web and inside the native app webview. Owned progress reporting, settings navigation, design-system components, store architecture, and automated test coverage.

  • SvelteKit
  • Svelte 5
  • TypeScript
  • Tailwind CSS
  • Vitest
  • Playwright

Child Progress Report

Owned the progress report end to end — the Parent Center UI and the PHP service-layer endpoints that power it. Assembled lifetime and period completion stats, a recent-activity feed, and per-category learning-path progress (units, lessons, current/next unit). On the client, designed skeleton, empty, and error states so the page stays clear while data loads or when a child has no activity yet.

  • Activity Report API
  • Learning Path Progress
  • Empty / Error States
// Portfolio-safe mocks mirroring the report + path response shapes

type ProgressStatus = 'not_started' | 'in_progress' | 'completed';

type RecentActivity = {
  activityId: number;
  title: string;
  category: string;
  imagePath: string;
  activityType: string;
  completedAt: string; // Y-m-d H:i:s
};

type UserReport = {
  userUuid: string;
  childName: string;
  lifetime: {
    activitiesCompleted: number;
    learningPathActivitiesCompleted: number;
  };
  // Defaults to last 7 days; optional custom start/end range
  period: {
    startDate: string; // Y-m-d
    endDate: string;
    totals: {
      activitiesCompleted: number;
      learningPathActivitiesCompleted: number;
      minutesPlayed: number;
    };
  };
  recentActivities: RecentActivity[]; // capped (e.g. 20)
};

type LessonProgress = {
  lessonId: string;
  title: string;
  status: ProgressStatus;
};

type UnitSummary = {
  unitNumber: number;
  unitId: string;
  title: string;
  lessonsCompleted: number;
  lessonsTotal: number;
  status: ProgressStatus;
};

type UnitProgress = {
  summary: UnitSummary;
  lessons: LessonProgress[];
};

type PathProgress = {
  category: 'reading' | 'math';
  grade: string;
  gradeDisplayName: string;
  gradeDescription: string;
  progressPercent: number;
  lessonsCompleted: number;
  lessonsTotal: number;
  unitsCompleted: number;
  unitsTotal: number;
  currentUnit: UnitProgress | null;
  nextUnit: UnitSummary | null;
  units: UnitProgress[];
};

type LearningPathProgress = {
  userUuid: string;
  paths: PathProgress[];
};

const userReport: UserReport = {
  userUuid: 'child_01',
  childName: 'Alex',
  lifetime: {
    activitiesCompleted: 142,
    learningPathActivitiesCompleted: 98
  },
  period: {
    startDate: '2026-03-10',
    endDate: '2026-03-16',
    totals: {
      activitiesCompleted: 12,
      learningPathActivitiesCompleted: 8,
      minutesPlayed: 95
    }
  },
  recentActivities: [
    {
      activityId: 1001,
      title: 'Letter Sounds',
      category: 'Reading',
      imagePath: '/mock/letter-sounds.png',
      activityType: 'game',
      completedAt: '2026-03-16 18:20:00'
    }
  ]
};

const learningPath: LearningPathProgress = {
  userUuid: 'child_01',
  paths: [
    {
      category: 'reading',
      grade: 'k',
      gradeDisplayName: 'Kindergarten',
      gradeDescription: 'Build letter sounds and early reading confidence.',
      progressPercent: 37.5,
      lessonsCompleted: 6,
      lessonsTotal: 16,
      unitsCompleted: 1,
      unitsTotal: 4,
      currentUnit: {
        summary: {
          unitNumber: 2,
          unitId: 'unit_abc',
          title: 'Short Vowels',
          lessonsCompleted: 1,
          lessonsTotal: 4,
          status: 'in_progress'
        },
        lessons: [
          {
            lessonId: 'lesson_01',
            title: 'A as in Apple',
            status: 'completed'
          },
          {
            lessonId: 'lesson_02',
            title: 'E as in Egg',
            status: 'in_progress'
          }
        ]
      },
      nextUnit: {
        unitNumber: 3,
        unitId: 'unit_def',
        title: 'Blending',
        lessonsCompleted: 0,
        lessonsTotal: 4,
        status: 'not_started'
      },
      units: [/* … */]
    }
  ]
};

Settings Shell & View Routing

Architected a responsive settings shell — stacked navigation on mobile, sidebar rail plus content panel on desktop — and used a single route with ?view= sub-views for deeplinkable account and profile flows. Shared back-navigation config keeps multi-step flows (edit profile, membership details) consistent without nesting a deep route tree.

  • Responsive Shell
  • Query View Slugs
  • Deeplinkable Flows
// Generic recreation of the view-map pattern

const settingsViews = {
  account: ['overview', 'update-email', 'membership'],
  'child-profiles': ['list', 'add-child', 'edit-child'],
  'app-settings': ['overview', 'language']
} as const;

type Section = keyof typeof settingsViews;
type ViewFor<S extends Section> = (typeof settingsViews)[S][number];

function settingsHref(section: Section, view: ViewFor<Section>) {
  return `/settings/${section}?view=${view}`;
}

// Example: /settings/child-profiles?view=edit-child
settingsHref('child-profiles', 'edit-child');

Child Profiles

Implemented add and edit child-profile flows with display name, birth date, and reading/math level pickers presented as bottom sheets, plus confirmation dialogs when a level change would affect the learning path. Kept profile forms driven by typed view models so validation and UI stay aligned.

  • Add / Edit Flows
  • Level Pickers
  • Confirmation Dialogs
type ChildProfile = {
  id: string;
  displayName: string;
  birthMonth: number | null;
  birthYear: number | null;
  readingLevelKey: string | null;
  mathLevelKey: string | null;
};

type GradeOption = { key: string; label: string };

const gradeOptions: GradeOption[] = [
  { key: 'prek', label: 'Pre-K' },
  { key: 'k', label: 'Kindergarten' },
  { key: 'grade-1', label: '1st Grade' }
];

const draft: ChildProfile = {
  id: 'child_01',
  displayName: 'Alex',
  birthMonth: 4,
  birthYear: 2019,
  readingLevelKey: 'k',
  mathLevelKey: 'prek'
};

Design System & Shared Components

Helped establish a Figma-sourced token pipeline into Tailwind so spacing, color, and type stayed consistent across Parent Center. Built and reused shared primitives — buttons, inputs, dialogs, sheets, and layout shells — so feature routes composed UI instead of reinventing patterns.

  • Design Tokens
  • Shared Primitives
  • Tailwind Theme
/* Genericized token → utility mapping */
:root {
  --color-surface: #0f172a;
  --color-accent: #2dd4bf;
  --space-panel: 1.5rem;
  --radius-card: 0.75rem;
}

/* Feature pages compose shared primitives */
<SettingsPanel title="Child profiles">
  <TextField label="Display name" bind:value={name} />
  <BottomSheet open={levelSheetOpen}>
    <GradePicker options={gradeOptions} onSelect={setLevel} />
  </BottomSheet>
</SettingsPanel>

Store Architecture

Structured client state around a base store and action pattern so UI components called typed actions instead of wiring fetch logic inline. Cross-cutting concerns — loading flags, retry, and debounce — lived on the action layer, which kept progress, profile, and settings views thinner and easier to test.

  • Base Store
  • Typed Actions
  • Loading / Retry
// Generic recreation — invented names, not production source

class BaseStore<T> {
  constructor(private state: T) {}
  get() { return this.state; }
  patch(partial: Partial<T>) {
    this.state = { ...this.state, ...partial };
  }
}

type ReportState = {
  report: UserReport | null;
  isLoading: boolean;
  error: string | null;
};

class LoadChildReport {
  constructor(private store: BaseStore<ReportState>) {}

  async run(childId: string) {
    this.store.patch({ isLoading: true, error: null });
    try {
      const report = await fetchReport(childId); // mocked in portfolio
      this.store.patch({ report, isLoading: false });
    } catch {
      this.store.patch({
        isLoading: false,
        error: 'Unable to load progress'
      });
    }
  }
}

Native Webview Bridge

Supported dual deployment: the same Parent Center UI runs as a standalone web app and as an embedded webview inside the native app. A thin bridge layer handled close-to-app navigation, device settings (microphone, notifications, language), and in-app vs browser presentation differences without leaking platform details into feature pages.

  • Dual Runtime
  • Bridge Events
  • Device Settings
// Generic bridge sketch

type BridgeEvent =
  | { type: 'CLOSE_PARENT_CENTER' }
  | { type: 'SET_LOCALE'; locale: string }
  | { type: 'SET_MIC_ENABLED'; enabled: boolean };

function postToNative(event: BridgeEvent) {
  window.parent?.postMessage({ source: 'parent-center', event }, '*');
}

function isInApp(): boolean {
  return document.documentElement.dataset.runtime === 'webview';
}

// Feature UI branches on runtime, not platform APIs
if (isInApp()) {
  postToNative({ type: 'CLOSE_PARENT_CENTER' });
} else {
  history.back();
}

Testing

Covered critical parent flows with Vitest unit and component tests for mappers, stores, and UI states, plus Playwright end-to-end coverage for settings navigation and profile editing. Used page-object helpers and fixtures so suite setup stayed readable as the surface area grew.

  • Vitest
  • Playwright
  • Page Objects
// Illustrative E2E structure (fictional selectors)

class ChildProfilesPage {
  constructor(private page: Page) {}

  async openEdit(childName: string) {
    await this.page.getByRole('button', { name: childName }).click();
    await this.page.getByRole('link', { name: 'Edit profile' }).click();
  }

  async setDisplayName(name: string) {
    await this.page.getByLabel('Display name').fill(name);
    await this.page.getByRole('button', { name: 'Save' }).click();
  }
}

test('parent can rename a child profile', async ({ page }) => {
  const profiles = new ChildProfilesPage(page);
  await page.goto('/settings/child-profiles?view=list');
  await profiles.openEdit('Alex');
  await profiles.setDisplayName('Alex R.');
  await expect(page.getByText('Alex R.')).toBeVisible();
});