← Back to Index

CardText - Data Source Analysis

Component: CardText
Work Item: WI-2127
Figma Key: card-md
Type: Domain control — Menu Text: composes the generic CardTextViewModel scaffold via MenuTextFactory (maps MenuItem → scaffold; does not subclass it — see Decisions D11)
Last Updated: 2026-06-05 (migrated to the domain-control pattern)


Favorite heart — rendered (design decision D31, 2026-06-26): The i-heart (18×18) IS drawn on the card. IsFavorite persists as client state (Settings.FavoriteItemIdsJSON).

ASCII Diagram — CardText Layout

Shows how CardTextViewModel properties map to the rendered control regions.

        ┌──────────────────────────────────────────────────────────┐
        │                                                          │
        │            [  img — 250×200px — fill:#000000  ]          │ ◄── ImageSource
        │                                                          │
        ├──────────────────────────────────────────────────────────┤
        │  content (250×122px)                                     │
        │                                                          │
   Label ──────────────►  MEC Group                                │ ◄── Label (optional — null → Title variant)
        │                                                          │
   Title ────────────►  United Airlines                            │ ◄── Title (string)
        │                                                          │
   Description ──────►  Lorem ipsum dolor sit amet, consectetur    │ ◄── Description (body text, string)
        │               adipiscing elit, sed do eiusmod tempor.    │
        │                                                          │
   Link ─────────────►  (tap navigates to destination)             │ ◄── Link (TBD — navigation path/URL)
        │                                                          │
        ├──────────────────────────────────────────────────────────┤
        │  status (250×26px)                                       │
        │                                                          │
   NewCount ─────────►  [ 11 ]  (red badge, white text)            │ ◄── NewCount (int — persisted to Settings, hidden when 0)
   IsFavorite ───────►  (favorite state — heart rendered)          │ ◄── IsFavorite (bool — persisted to Settings.FavoriteItemIdsJSON)
        │                                                          │
        └──────────────────────────────────────────────────────────┘

   IsLabelVisible ───►  Controls visibility of label row         ◄── !string.IsNullOrEmpty(Label)
   IsLinkVisible ────►  Controls visibility of link row            ◄── ⚠️ TBD
   IsLoading ────────►  Default strategy — no skeleton/overlay

   ✅ All properties resolved — Description + NewCount via MenuItem API expansion; IsFavorite via local storage

Note: The CardText has three layout variants, named by their leading element and driven by property presence (simplest-first). These map to a TextCardVariant enum: - Description (TextCardVariant.Description): Description + Link only (Label and Title not set) - Title (TextCardVariant.Title): Title + Description + Link (Label not set) - Label (TextCardVariant.Label): Label + Title + Description + Link


Summary

Property Status Source Notes
Label ✅ Available MenuItem.Title of parent category (MEC group, airline, section) Optional — null/empty collapses label row
Title ✅ Available MenuItem.Title Primary content heading
Description ✅ Resolved MenuItem.Description (new field — backend API expansion) Add Description string to MenuItem model + API response
ImageSource ✅ Available MenuItem.ImageSource Falls back to glyph-derived FontImageSource when empty
Link ✅ Available MenuItem.Path Navigation path/URL — opened via SharedActionsService or Shell navigation
NewCount ✅ Resolved Settings.NewCountsJSON (local Microsoft.Maui.Storage.Preferences) Client-computed only — not backend-provided. Persisted as JSON dict { itemId: count }
IsFavorite ✅ Resolved Settings.FavoriteItemIdsJSON (local Microsoft.Maui.Storage.Preferences) Persisted as JSON array of MenuItem.Id — same pattern as FlightSearchNotifyMeList
IsLabelVisible ✅ Available Derived: !string.IsNullOrEmpty(Label) Drives Label vs. Title/Description layout
IsLinkVisible ⚠️ TBD ViewModel state Depends on whether Link is populated
IsLoading ✅ Available ViewModel state Default strategy (no skeleton)

✅ IsFavorite + NewCount — Resolved via Local Storage

Follow the existing Settings.cs pattern (ALPAMobile/Helpers/Settings.cs). Add two new keys:

// In Settings.cs
private const string FavoriteItemIdsJSONKey = "text_card_favorite_item_ids_json_key";
private const string NewCountsJSONKey = "text_card_new_counts_json_key";

/// Persisted list of favorited MenuItem.Id values
public static string FavoriteItemIdsJSON
{
    get => Microsoft.Maui.Storage.Preferences.Get(FavoriteItemIdsJSONKey, "[]");
    set => Microsoft.Maui.Storage.Preferences.Set(FavoriteItemIdsJSONKey, value);
}

/// Persisted dict of { menuItemId: newCount } for badge display
public static string NewCountsJSON
{
    get => Microsoft.Maui.Storage.Preferences.Get(NewCountsJSONKey, "{}");
    set => Microsoft.Maui.Storage.Preferences.Set(NewCountsJSONKey, value);
}

Helper methods on the domain service (MenuTextService) — persistence lives here, not on the scaffold VM:

public void ToggleFavorite()
{
    var favorites = JsonConvert.DeserializeObject<List<int>>(Settings.FavoriteItemIdsJSON) ?? new();
    if (IsFavorite) favorites.Remove(Id);
    else favorites.Add(Id);
    IsFavorite = !IsFavorite;
    Settings.FavoriteItemIdsJSON = JsonConvert.SerializeObject(favorites);
}

public static bool GetIsFavorite(int itemId)
{
    var favorites = JsonConvert.DeserializeObject<List<int>>(Settings.FavoriteItemIdsJSON) ?? new();
    return favorites.Contains(itemId);
}

public static int GetNewCount(int itemId)
{
    var counts = JsonConvert.DeserializeObject<Dictionary<int, int>>(Settings.NewCountsJSON) ?? new();
    return counts.TryGetValue(itemId, out var count) ? count : 0;
}

NewCount write path (incrementing/clearing the badge) is a separate concern — likely driven by push notification receipt or document sync. The Settings.NewCountsJSON key provides the read path for display.


✅ Description + NewCount — MenuItem API Expansion (Decision: 2026-06-02)

Architecture decision: Expand MenuItem rather than DocumentItem. CardText is a navigation tile with richer display — MenuItem is the source of truth for what appears on the home screen. Adding Description and NewCount to the MenuItem response keeps the data flow simple and avoids a second async query per card.

DocumentCardViewModel remains the correct base for CardHero and future pure-content cards. CardTextViewModel stays MenuItem-backed.

Proposed MenuItem model addition:

// In ALPADocs.Data.Models.MenuItem
public string Description { get; set; } = string.Empty;  // card body text

Backend work required: Add description to the MenuItem API response. Coordinate with backend team.

NewCount is client-computed only — not backend-provided. Persisted entirely in Settings.NewCountsJSON. Write path driven by local events (push notification receipt, document sync, etc.).


⚠️ NewCount (Status Badge) — Write Path TBD

Read path resolved via Settings.NewCountsJSON. The write path (who increments/clears the count) still needs a decision:

  1. Push notification receipt — increment on incoming push; clear on tap/view (most likely)
  2. Document sync — compare cached vs. server document list on refresh
  3. Separate badge API endpoint — backend provides count directly

Current recommendation: Wire read path to Settings.NewCountsJSON now. Write path TBD with backend/product team.


✅ IsFavorite (Heart Icon) — Resolved via Local Storage

Persist favorited MenuItem.Id values as a JSON array in Settings.FavoriteItemIdsJSON (same pattern as FlightSearchNotifyMeList). No backend required for MVP. See helper methods above.


Mapping — Factory + Domain Service

The Menu Text domain control keeps the generic CardTextViewModel scaffold domain-free; a pure factory maps a MenuItem (plus resolved client state) onto it; a domain service reads the local favorite / new-count state and owns the toggles, then calls the factory (decision D11).

// Scaffold — generic text card, domain-free
public class CardTextViewModel : CardViewModel
{
    public string? Label { get; set; }                 // null/empty → Title variant
    public string Title { get; set; } = string.Empty;
    public string? Description { get; set; }
    public ImageSource? ImageSource { get; set; }
    public string? Link { get; set; }
    public int NewCount { get; set; }
    public bool IsFavorite { get; set; }
    public bool IsLabelVisible => !string.IsNullOrEmpty(Label);
    public bool IsLinkVisible => !string.IsNullOrEmpty(Link);
    public bool IsNewCountVisible => NewCount > 0;
}

// Domain control — PURE MAPPER: MenuItem (+ resolved client state) in, scaffold VM out
public sealed class MenuTextFactory : RawRepresentationFactory<CardTextViewModel>
{
    public CardTextViewModel Create(MenuItem item, MenuItem? parent, int newCount, bool isFavorite) => new()
    {
        Label       = parent?.Title,        // parent category title (e.g. MEC Group)
        Title       = item.Title,
        Description = item.Description,      // new MenuItem field — empty until backend adds it
        Link        = item.Path,
        NewCount    = newCount,
        IsFavorite  = isFavorite,
        ImageSource = ResolveImage(item),
    };

    // Prefer FontImageSource from glyph; fall back to string asset
    private static ImageSource? ResolveImage(MenuItem item) =>
        !string.IsNullOrEmpty(item.Glyph) && !string.IsNullOrEmpty(item.GlyphFontFamily)
            ? MenuItemViewModel.GetImageFromGlyph(item.Glyph, item.GlyphFontFamily)
            : string.IsNullOrEmpty(item.ImageSource) ? null : item.ImageSource;
}

// Domain service — resolves client state (Settings) + owns toggles, then maps
public sealed class MenuTextService(MenuTextFactory factory)
{
    public CardTextViewModel Build(MenuItem item, MenuItem? parent = null) =>
        factory.Create(item, parent,
            newCount:   GetNewCount(item.Id),      // read from Settings.NewCountsJSON
            isFavorite: GetIsFavorite(item.Id));   // read from Settings.FavoriteItemIdsJSON

    // GetNewCount / GetIsFavorite / ToggleFavorite live here (persistence concern), not on the scaffold VM.
}

Properties

Label

Title

Description

ImageSource

NewCount

IsFavorite

IsLabelVisible

IsLinkVisible

IsLoading


Data Dependencies

Service / Model Interface / Class Notes
Menu Data MenuItem (Data.Models) All card properties: Title, Description (new), NewCount (new), Path, ImageSource, Glyph
Menu ViewModel MenuItemViewModel Glyph → FontImageSource helper reused
Navigation Shell / SharedActionsService Resolves MenuItem.Path to navigation action
Push Notifications PushNotificationsListPageViewModel Write path candidate for NewCount increment — TBD
Local Settings Settings.cs (Microsoft.Maui.Storage.Preferences) FavoriteItemIdsJSON + NewCountsJSON — same pattern as JumpseatSavedFlightsJSONSettings


Document Owner: ALPA Mobile Team
Work Item: WI-2127
Purpose: Data source analysis for CardText component — presentation-layer implementation reference