Visual schedule, food choice, and activity choice app for children with SEN (Special Educational Needs). Displays pictures in Now/Then/Next format to communicate daily plans and offers visual choice boards for food and activities.
- Framework: Blazor WebAssembly (WASM) - .NET 10 RC (preview)
- Language: C# / .NET
- Styling: Tailwind CSS CDN with custom SEN-friendly theme
- Storage: Browser IndexedDB via
wwwroot/js/indexeddb.js(base64 encoded images, hundreds of MB capacity) - Testing: Playwright E2E tests with xUnit v3
- Font: Nunito (Google Fonts) - rounded, friendly typography
- Calm & Clear: Soft colors (teal primary #5B9A9A, muted green accent #7BA893, phonics blue #7BA3C4, off-white bg #F9F7F3), generous spacing, minimum 48px touch targets
- No Harsh Colors: Avoid pure red/bright yellow - use calm amber #D4A06A for warnings
- Accessibility: WCAG AA contrast (~8.5:1 ratio), large fonts (18px base, 24px+ labels), rounded sans-serif
- Simplicity: Minimal UI, no distractions, straightforward navigation
- Visual-First: Large images, clear icons, visual feedback on interactions
- Slow Animations: Calming, gentle transitions (3s pulse, 7s auto-advance)
src/NowThenNext/ # Main Blazor WASM application
├── Pages/ # Razor pages for each view
├── Components/ # Reusable Blazor components
├── Models/ # Data models (ImageItem, etc.)
├── Services/ # Business logic (ImageStorageService)
└── wwwroot/ # Static assets
tests/NowThenNext.Tests.E2E/ # E2E tests with Playwright
- Image Libraries: Places, Food, Activities
- Schedule Builder: Select up to 3 places for Now/Then/Next display
- Choice Boards: Food Choices and Activity Choices (2-6 items)
- Favorites: Cross-category favorites view
- Backup/Restore: Export/import data as JSON
- Phonics Flashcards: UK Letters and Sounds Phases 2-5 with sequential unlocking and progress tracking
- Branch:
ralph/phonics-flashcards(phonics feature in progress) - Completed: US-001 through US-037 (37/37 user stories - MVP COMPLETE)
- Phonics: US-038 through US-048 complete (13/13 phonics stories - PHONICS COMPLETE)
- Activities category fully implemented with library, choices selection, display, and E2E tests
prd.json- Full PRD with all 37 user storiesprogress.txt- Detailed implementation history and learningssrc/NowThenNext/Pages/Home.razor- Main menu/navigationsrc/NowThenNext/Services/LocalStorageImageService.cs- Data persistence layersrc/NowThenNext/wwwroot/index.html- Tailwind config & JS interop functionstests/NowThenNext.Tests.E2E/- E2E test suite (54 tests)
# Run application
dotnet run --project src/NowThenNext
# Run E2E tests
dotnet test tests/NowThenNext.Tests.E2E
# Build project
dotnet build- Category-based architecture: All images categorized as Places, Food, or Activities
- IndexedDB JSON storage: Images stored as base64 JSON strings in IndexedDB (one record per store, key
"data") - Reusable components: ImageTile component shared across all libraries
- Parallel workflows: Food Choices and Activity Choices follow same pattern as templates
- Static data service: Phonics GPC data is hardcoded in
PhonicsDataService(singleton), progress tracked inPhonicsProgressService(scoped, uses IndexedDB storephonics-progress)
| Route | Page | Description |
|---|---|---|
/ |
Home.razor | Main menu: Places, Food, Activities, Plan the Day, Food Choices, Activity Choices, Favorites, Learning Cards |
/places |
PlacesLibrary.razor | Places image library |
/food |
FoodLibrary.razor | Food image library |
/activities |
ActivitiesLibrary.razor | Activities image library |
/upload |
Upload.razor | Image upload (optional category param) |
/upload/{category} |
Upload.razor | Image upload with pre-selected category |
/plan |
PlanDay.razor | Schedule selection (Now/Then/Next) |
/schedule |
ScheduleDisplay.razor | Schedule display (?ids=id1,id2,id3) |
/food-choices |
FoodChoices.razor | Food choice selection |
/food-display |
FoodDisplay.razor | Food display (?ids=id1,id2,...) |
/activity-choices |
ActivityChoices.razor | Activity choice selection |
/activity-display |
ActivityDisplay.razor | Activity display (?ids=id1,id2,...) |
/favorites |
Favorites.razor | Favorited images from all categories |
/settings |
Settings.razor | Backup/restore data |
/phonics |
PhonicsPhases.razor | Phonics phase selection (Phase 2-5) |
/phonics/{phaseId} |
PhonicsSoundList.razor | Sound list within a phase |
/phonics/{phaseId}/card/{graphemeId} |
PhonicsCard.razor | Flashcard display with navigation |
public class ImageItem
{
public string Id { get; set; } // GUID
public string Base64Data { get; set; } // Base64 image data
public string Label { get; set; } // Optional label
public ImageCategory Category { get; set; } // Places/Food/Activities
public bool IsFavorite { get; set; } // Favorited flag
public DateTime CreatedAt { get; set; } // Creation timestamp
}public enum ImageCategory
{
Places = 0,
Food = 1,
Activities = 2 // Added in US-032
}@inject IImageStorageService ImageStorage
@inject IJSRuntime JSRuntime
@inject NavigationManager Navigation
@inject IPhonicsDataService PhonicsData
@inject IPhonicsProgressService PhonicsProgress<!-- Use calm-* classes defined in Tailwind config (in wwwroot/index.html) -->
<div class="bg-calm-bg text-calm-text">
<button class="bg-calm-primary hover:bg-calm-primary-dark">// Compress to max 800px, 0.7 quality
var compressed = await JSRuntime.InvokeAsync<string>(
"compressImage", dataUrl, 800, 0.7);// Get by category
var images = await ImageStorage.GetImagesByCategoryAsync(ImageCategory.Places);
// Save image
await ImageStorage.SaveImageAsync(new ImageItem { ... });
// Delete image
await ImageStorage.DeleteImageAsync(imageId);
// Toggle favorite
await ImageStorage.ToggleFavoriteAsync(imageId);
// Check storage usage
var info = await ImageStorage.GetStorageInfoAsync();
// Returns: UsagePercentage, EstimatedRemainingImages, CurrentUsageBytes, EstimatedQuotaBytes[Collection("BlazorApp")]
public class MyTests(BlazorAppFixture fixture)
{
[Fact]
public async Task MyTest()
{
var page = fixture.Page;
await page.GotoAsync(fixture.BaseUrl);
await page.ClearLocalStorageAsync(); // Clear AFTER navigating
// ... test logic
}
}- Use
@@keyframes(double @) for CSS animations in<style>blocks - Arrow in title uses Unicode:
→(displayed as "Now → Then → Next")
- Use
min-width: 0on grid items to allow proper shrinking in CSS Grid - Fixed columns (
repeat(3, 1fr)) often better thanauto-fillfor consistency - Tablet breakpoints: 768px (md:) and 1024px (lg:)
- Boolean flag for visibility (e.g.,
ShowDeleteConfirmation) @onclick:stopPropagation="true"on modal content to prevent backdrop clicks
- Use
InputFilecomponent withacceptattribute OpenReadStream(maxSize)to read files (50MB max for restore)- Convert to base64 via
MemoryStreamandConvert.ToBase64String
- Schedule (PlanDay): Max 3 items, numbered badges (1=Now, 2=Then, 3=Next)
- Food Choices: Min 2 items, no max limit, checkmark badges
- Activity Choices: Min 2 items, no max limit, checkmark badges
- GraphemeCard ID format:
p{phase}-w{week}-{grapheme}(e.g.,p2-w1-s,p3-w2-oo_long,p5-w3-ch_chef) - Duplicate grapheme IDs: When multiple cards share a grapheme (e.g., oo long/short, alternative pronunciations), use suffixed IDs and manual card construction instead of the
BuildWeekhelper - Sequential unlocking: First grapheme in each phase always unlocked; completing one unlocks the next by
OrderIndex - Progress storage:
HashSet<string>of completed card IDs serialized as JSON in IndexedDB storephonics-progress - Three sound tile states: Completed (muted bg + checkmark), Current (phonics-blue border), Locked (grey + lock icon,
<div>not<a>) - Phonics color:
calm-phonics: #7BA3C4,calm-phonics-dark: #6890B0,calm-phonics-light: #9BBDD6
- When navigating between pages with the same route template (e.g.,
/phonics/2/card/Ato/phonics/2/card/B), Blazor reuses the component instance OnInitializedAsyncdoes NOT re-run - useOnParametersSetAsyncinstead for pages with changing route parameters- Reset any local state flags (like
ShowPhaseComplete) at the start ofOnParametersSetAsync
- Blazor WASM needs extra init time - use 60s timeout on
WaitForSelectorAsync - Use
WaitForURLAsync()for navigation assertions - Clear storage (IndexedDB + localStorage) AFTER navigating to page (requires page context)
- Note: FileChooser tests can be flaky due to Playwright/Blazor InputFile interaction
- Use RELATIVE paths in href selectors (e.g.,
a[href='phonics']NOTa[href='/phonics']) - Blazor renders relative hrefs in the DOM
- Test Files: HomePageTests, UploadTests, LibraryTests, ScheduleTests, FoodChoicesTests, ActivityChoicesTests, BackupRestoreTests, PhonicsTests, LearningCardsTests
- Coverage: All user stories (US-001 to US-037, US-046 to US-048)
When debugging 404s, routing errors, or broken assets in this app, check causes in this order:
- Base path / relative URLs - Are all URLs relative or respecting
<base href>? This is the #1 cause of issues on GitHub Pages subdirectory deployments. - Case sensitivity - File/path case mismatches between code and filesystem.
- Build output - Is the expected file actually in the publish output?
- Only after ruling out the above, investigate deeper causes (service workers, framework internals, server config).
This app deploys to GitHub Pages at a subdirectory path (/NowThenNext/).
- All asset references, navigation links, and API calls must use relative paths or respect
<base href> - Never use absolute paths starting with
/in HTML, Razor, or JS - they will 404 on GitHub Pages - The
<base href>is set to/for dev and rewritten to/NowThenNext/during CI publish - After any bulk path changes, grep the entire project for remaining absolute paths:
href="/andsrc="/
- Always maintain calm visual design - avoid bright/harsh colors
- Keep touch targets ≥48px for accessibility
- Test in browser for visual verification
- E2E tests required for all user-facing features
- Storage quota warnings at 80% capacity
- Dev server runs on localhost:5161
- Use
pkill -f "dotnet"to kill stray processes before E2E tests - Responsive padding pattern:
px-6 py-8mobile,sm:px-8 sm:py-10larger screens - After bulk edits across multiple files, always verify with grep that no stale instances of the old pattern remain