---
name: card-view
description: Create a new card view, or migrate an existing legacy card view, to the BaseCardFactory<T, C> pattern
disable-model-invocation: true
argument-hint: "<CardTypeName or card_name> e.g. FCommunity_SpaceMemberInvitation"
---

# Card View — Create or Migrate

Handle card view work for `$ARGUMENTS` using the target architecture: `BaseCardFactory<T, C>` + `BaseCard<T>`.

The skill branches based on whether the card already exists in the legacy pattern:

- **CREATE** — no folder for this card exists yet in this repo → build from scratch
- **MIGRATE** — folder exists under `lib/presentation/core/renderer/card_views/<card_name>/` with the legacy `CardViewFactory` pattern (`designs/`, `entities/`, `<name>_factory.dart`) → port to `BaseCardFactory`

Both paths converge on the same output structure; only Step 2 differs.

## Context

Appza_community is partway through migrating card views from the legacy `CardViewFactory` (designs/entities/factory tree) to `BaseCardFactory<T, C>` + `BaseCard<T>`. Many cards are already on the new pattern (`feed/feed_card_new/`, `space/space_card_views/`, `member/member_card_views/`, `chat/chat_add_member_view/`, `course/course_*`, `course/lesson_*`); others are still legacy (`chat/chat_card_views/`, `comment/comment_card_views/`, `feed/create_post_card_views/`, `document/document_card_views/`, `drawer/drawer_*`, `feed/feed_banner_card_views/`, etc.).

New cards MUST use the new pattern. Legacy cards should be migrated one at a time.

### Page-grouped folder structure

`lib/presentation/core/renderer/card_views/` is organized into **page-family parent folders**. Every card lives under one of these (pick the closest match for new cards):

`chat/`, `comment/`, `course/`, `document/`, `drawer/`, `feed/`, `leaderboard/`, `member/`, `notification/`, `playground/`, `schedule_post/`, `space/`

Three folders stay flat at the top:
- `base/` — `OrientationCardView`, etc.
- `shared/` — `BaseCardFactory`, `BaseCard`
- `_example_new_architecture/` — `CardRegistry`-based exploration (keep separate; not the target pattern of this skill)

When creating or migrating, the full path is `card_views/<page_family>/<card_name>/`, e.g., `card_views/space/space_member_invitation_card_views/`.

The sibling repo `../fc_mobile_stand_alone/` has its own copy of these card views — often on the legacy pattern still (with UI/logic improvements that haven't been ported yet). When migrating, cross-check standalone for newer implementations to port.

## Step 1: Study reference implementations FIRST

Read these before writing any code:

1. **Contract files (this repo):**
   - `lib/presentation/core/renderer/card_views/shared/base_card.dart` — `BaseCard<T>` abstract class + shared `executeAction` API helper
   - `lib/presentation/core/renderer/card_views/shared/base_card_factory.dart` — `BaseCardFactory<T, C>` abstract class + `CardFactoryException`

2. **Simplest full example — study factory + base + one design:**
   - `lib/presentation/core/renderer/card_views/space/space_card_views/space_card_factory.dart`
   - `lib/presentation/core/renderer/card_views/space/space_card_views/shared/space_card_base.dart`
   - `lib/presentation/core/renderer/card_views/space/space_card_views/design_1/vertical.dart`
   - `lib/presentation/core/renderer/card_views/space/space_card_views/models/space_list_entity.dart` (@JsonSerializable entity)

3. **More patterns — pick based on complexity:**
   - `course/course_card_views/` — `createModel` unwrapping nested API format
   - `course/lesson_discussion/` — comment-style card with actions
   - `feed/feed_card_new/` — reactive `FeedCardState` + `FeedStateRegistry` shared across card instances

If `../fc_mobile_stand_alone/` is not cloned as a sibling directory and you need to port UI/logic from standalone, ask the user to clone it before continuing.

## Step 2A: CREATE path — gather requirements

If `lib/presentation/core/renderer/card_views/<page_family>/<card_name>/` does NOT exist (and no equivalent legacy folder exists under any page-family parent), you're creating from scratch.

Never guess. Confirm each of these with the user or the task description:

- **`class_type` identifier** — exact string the server sends (e.g., `FCommunity_SpaceMemberInvitation`)
- **Page family** — which parent folder does this card belong under? (`chat/`, `comment/`, `course/`, `document/`, `drawer/`, `feed/`, `leaderboard/`, `member/`, `notification/`, `playground/`, `schedule_post/`, `space/`)
- **Where it renders** — which page config JSON references this class_type? (search `lib/presentation/core/config/theme/page_josn/`)
- **Data source** — `args['item']` from config, a server endpoint, or session/auth state?
- **Entity shape** — which fields? Does an existing entity in `data/models/` or another card's `models/` folder already fit?
- **Actions** — which buttons/taps trigger what? Which API endpoints? Which `*Service` class in `data/services/api_services/` owns those endpoints?
- **Designs & orientations** — usually design 1 + vertical. Only build horizontal/grid if the config declares them.
- **UI reference** — screenshot, Figma, or existing similar card to mirror?

Skip to Step 3.

## Step 2B: MIGRATE path — investigate the legacy card

If `lib/presentation/core/renderer/card_views/<page_family>/<card_name>/` already exists with the legacy pattern, you're migrating. (If you only know the card name, grep for it under `card_views/` to locate the page family.)

1. Read `<card_name>/<card_name>_factory.dart` — note how it dispatches designs/orientations and what `onTap` does
2. Read `<card_name>/designs/base_orientation_view.dart` — note the action methods (these become `static` methods on the new base card)
3. Read `<card_name>/designs/<card_name>_card_view_1/orientations/vertical.dart` (and horizontal/grid if present) — note the UI + how it calls actions
4. Read `<card_name>/entities/*.dart` — decide whether to keep the existing manual `fromJson` entity, or upgrade to `@JsonSerializable` during the migration
5. Check `get_selected_card_view.dart` to see the current switch-case registration
6. **Cross-check `../fc_mobile_stand_alone/lib/presentation/core/renderer/card_views/<card_name>/`** — standalone often has newer UI/logic. Port the newer version, converting to the new pattern as you go.
7. Grep for other files importing from the old paths — they'll need updating in Step 6

**Migration rules:**
- Action methods from the legacy `base_orientation_view.dart` become `static` methods on the new `<CardName>Card` base class
- Raw `postCommunityData` / `getCommunityData` calls in the legacy code MUST be replaced with `*Service` classes (check `data/services/api_services/`; create a new service if the endpoint isn't covered)
- Do NOT delete the legacy folder in the same change — leave it in place and remove the `_getFactory()` switch entry only. A separate cleanup step (`/cleanup-old-code`) removes the old files once nothing imports them.

## Step 3: Directory structure to create

```
lib/presentation/core/renderer/card_views/<page_family>/<card_name>/
  <card_name>_factory.dart         # extends BaseCardFactory<Entity, CardBase>
  shared/
    <card_name>_base.dart          # extends BaseCard<Entity>, static actions + navigation
  design_1/
    vertical.dart                  # extends <CardName>Card, build() returns widget
    horizontal.dart                # only if config uses horizontal orientation
    grid.dart                      # only if config uses grid orientation
  models/
    <entity>_entity.dart           # @JsonSerializable OR manual fromJson — pick one style and stick with it
    <entity>_entity.g.dart         # generated (only if @JsonSerializable)
```

Use `snake_case` for folder/file names, `PascalCase` for classes. The folder name typically mirrors the `class_type` minus the `FCommunity_` prefix (e.g., `FCommunity_SpaceMemberInvitation` → `space/space_member_invitation_card_views/`).

**MIGRATE path note:** if the legacy folder already exists at `<page_family>/<card_name>/`, you can add `shared/`, `design_1/`, `models/` alongside the legacy `designs/` and `entities/` folders, then remove the legacy subfolders in the cleanup step.

## Step 4: Implement in this order

### 4a. Entity / model

**Preferred: `@JsonSerializable` with the standard converter bundle.** Codegen is set up in this repo (`json_annotation`, `json_serializable`, `build_runner` in `pubspec.yaml`), and `lib/core/utils/json_converters.dart` provides the `kApiConverters` bundle (`StringConverter`, `IntConverter`, `BoolConverter`, `DoubleConverter`, `MapConverter`). Apply it at the class level so you don't have to decorate each field:

```dart
import 'package:fc_mobile_stand_alone/core/utils/json_converters.dart';
import 'package:json_annotation/json_annotation.dart';

part '<entity>_entity.g.dart';

@JsonSerializable(converters: kApiConverters, fieldRename: FieldRename.snake)
class <Entity> {
  final int? id;
  final String? displayName;  // auto-maps to `display_name`

  @JsonKey(name: 'ID')        // only annotate when the key is irregular
  final int? userId;

  const <Entity>({this.id, this.displayName, this.userId});

  factory <Entity>.fromJson(Map<String, dynamic> json) =>
      _$<Entity>FromJson(json);

  Map<String, dynamic> toJson() => _$<Entity>ToJson(this);
}
```

- `converters: kApiConverters` — makes the model resilient to backend type quirks (`"5"` string for an int, `1`/`0` for a bool, `[]` instead of `{}` for an empty map, etc.) without per-field annotations
- `fieldRename: FieldRename.snake` — automatic `camelCase ↔ snake_case`, so `@JsonKey` is only needed for irregular names
- **Lists**: `kApiConverters` doesn't cover `List<T>` because the converter must match the concrete `T`. For fields where the backend may send `null` / `false` / `""` / `{}` instead of `[]`, use the `readListValue` hook from `json_converters.dart`:
  ```dart
  @JsonKey(readValue: readListValue)
  final List<Thing>? things;
  ```

- **No kApiConverters exemplar yet in `card_views/`** — existing entities use older patterns (see below). New cards should adopt `kApiConverters` + `fieldRename` and become the reference. Cross-check `../fc_mobile_stand_alone/lib/presentation/core/renderer/card_views/space_member_invitation_card_views/models/space_member_invitation_entity.dart` for a working example.

After writing/editing a `@JsonSerializable` file, run:
```
fvm dart run build_runner build --delete-conflicting-outputs
```

**Fallback: manual `fromJson` / `toJson`.** Use only when the entity is trivial (1–2 fields, no type coercion needed) AND you want zero codegen overhead. Reference: `feed/feed_card_new/models/feed_entity.dart`.

**Legacy patterns you'll encounter in existing entities (don't copy for new ones):**
- Per-field converters (`@IntConverter()`, `@BoolConverter()`, etc.) with explicit `@JsonKey(name: '...')` — see `course/course_content/models/lesson_model.dart`.
- `@JsonSerializable(fieldRename: FieldRename.snake)` with no converters — see `course/course_instructor/models/course_instructor_model.dart`. Fine when the backend is known to send clean types, but `kApiConverters` is safer.

Reuse existing entities when possible — check `data/models/`, `feed/feed_card_new/models/`, and other card `models/` folders before duplicating.

**MIGRATE path:** if the legacy entity is already a clean manual `fromJson` class, you can move it into `models/` unchanged. Upgrade to `@JsonSerializable` when the entity has more than a handful of fields or would benefit from `kApiConverters` type-coercion.

### 4b. Base card class — `shared/<card_name>_base.dart`

```dart
abstract class <CardName>Card extends BaseCard<<Entity>> {
  @override
  String get cardType => '<card_name>';  // debug/log identifier

  // STATIC action methods — callable from any design widget
  static Future<void> <actionName>({
    required Map<String, dynamic> args,
    // ...typed parameters
  }) async {
    // Use executeAction from BaseCard for API calls with loading/error handling,
    // or inline for custom flow (confirmation modal → API call → snackbar → onRefresh).
  }

  // STATIC navigation helpers
  static void navigateToDetails(Map<String, dynamic> args) { ... }
}
```

Rules:
- All action methods are `static` so designs can call them without holding a card instance
- Use `*Service` classes from `data/services/api_services/` for API calls — never raw `postCommunityData` / `getCommunityData`
- Use `colorPrint` for debug traces — never raw `print`
- If the card needs reactive state shared across instances (e.g. list + details screens for the same item), introduce a `<CardName>State` class + `<CardName>StateRegistry` following the `FeedCardState` / `FeedStateRegistry` pattern

### 4c. Design widgets — `design_1/vertical.dart` (and siblings)

```dart
class <CardName>Design1Vertical extends <CardName>Card {
  @override
  Widget build(
    <Entity> model,
    Map<String, dynamic> styleMap,
    Map<String, dynamic> args,
  ) {
    return InkWell(
      onTap: () => <CardName>Card.navigateToDetails(args),
      child: Container(
        margin: parseMargin(styleMap['box_decoration']),
        padding: parsePadding(styleMap['box_decoration']),
        decoration: parseBoxDecoration(jsonMap: styleMap['box_decoration']),
        child: /* ...widget tree using model + styleMap... */,
      ),
    );
  }
}
```

Rules:
- Each design file is a separate class extending the base card
- Read colors/sizes/fonts from `styleMap` via `parseMargin` / `parsePadding` / `parseBoxDecoration` / `parseTextStyle` / `parseImageDecoration` — don't hardcode unless the config doesn't expose the value
- Wire button `onPressed` / `onTap` to static action methods on the base card
- Wrap reactive fields in `Obx` only if the base card exposes `Rx<...>` state

### 4d. Factory class — `<card_name>_factory.dart`

```dart
class <CardName>CardFactory extends BaseCardFactory<<Entity>, <CardName>Card> {
  @override
  <CardName>Card createCardInstance(int selectedDesign, ListOrientation orientation) {
    return switch ((selectedDesign, orientation)) {
      (1, ListOrientation.vertical) => <CardName>Design1Vertical(),
      (1, ListOrientation.horizontal) => <CardName>Design1Horizontal(),
      (1, ListOrientation.grid) => <CardName>Design1Grid(),
      (_, _) => <CardName>Design1Vertical(),  // safe fallback
    };
  }

  @override
  <Entity> createModel(rawData) => <Entity>.fromJson(rawData);

  @override
  String get factoryType => '<card_name>';

  static Widget create<CardName>Card({
    required int selectedDesign,
    required ListOrientation orientation,
    required dynamic item,
    required Map<String, dynamic> styleMap,
    required Map<String, dynamic> args,
  }) {
    final factory = <CardName>CardFactory();
    return factory.buildCard(
      selectedDesign: selectedDesign,
      orientation: orientation,
      rawItem: item,
      styleMap: styleMap,
      args: args,
    );
  }
}
```

- If the backend wraps your entity in a nested shape (e.g. `{ "data": { ... } }`), unwrap inside `createModel` — see `course/course_card_views/`.
- Only override `buildCard` if you need an extra dispatch dimension on top of `(design, orientation)` — see `feed/feed_card_new/feed_card_factory.dart` for a `layout_mode` override example.

## Step 5: Wire into GetSelectedCardView

Open `lib/presentation/core/renderer/card_views/get_selected_card_view.dart`.

Add an **if-block inside `build()`** (not a switch case) — new-pattern cards bypass the legacy `_getFactory()` switch:

```dart
@override
Widget build(BuildContext context) {
  if (classType == 'FCommunity_Feed') { /* existing */ }

  if (classType == '<Your_ClassType>') {
    final item = args['item'];
    final styleMap = args['style_map'] ?? {};
    final orientation = args['orientation'] as ListOrientation;
    return <CardName>CardFactory.create<CardName>Card(
      selectedDesign: selectedDesign,
      orientation: orientation,
      item: item,
      styleMap: styleMap,
      args: args,
    );
  }

  // ...other if-blocks...
  final factory = _getFactory(classType);
  return factory.createCardView(designNumber: selectedDesign, args: args);
}
```

Do NOT add the card to the `_getFactory()` switch — that's the legacy dispatch.

Add the import at the top of the file.

**MIGRATE path:** also REMOVE the matching `case` entry from the `_getFactory()` switch, and delete the legacy factory's import. Leave the legacy folder on disk; `/cleanup-old-code` removes it in a separate change.

## Step 6: Update external imports

Grep for the old import paths — any file that used to import from the legacy `<card_name>/designs/` or `<card_name>/entities/` needs updating to the new `design_1/` or `models/` paths.

```
rg "from.*<card_name>/designs" lib/
rg "from.*<card_name>/entities" lib/
```

## Step 7: Add the page config entry (CREATE path only)

If the server config isn't already sending this `class_type` to the app, add a view block to the relevant file under `lib/presentation/core/config/theme/page_josn/` — copy the shape of a neighboring view and adjust `class_type`, `styles`, `items`, `filters`, `pagination` as needed. (Note: `page_josn/` is the actual folder name, inherited typo'd from fc_mobile_stand_alone.)

## Step 8: Verify

Every verification step is required — do not skip any:

1. **Static analysis** — `fvm flutter analyze <new-files>` returns zero new errors/warnings
2. **Grep** — search for the `class_type` string and legacy paths across the codebase; confirm nothing else needs updating
3. **Build** — build an APK, install on the connected device
4. **Manual test** — give the user exact steps to navigate to the page, verify the golden path and edge cases (empty, loading, error, action success/failure)

## Rules

- **New or migrated cards MUST use `BaseCardFactory<T, C>`** — do not add to the legacy `_getFactory()` switch
- Use `colorPrint` for debug traces, never raw `print`
- Use `*Service` classes from `data/services/api_services/` for API calls, never raw `getCommunityData` / `postCommunityData`
- All action methods on the base card are `static`
- MIGRATE path: do NOT delete the legacy folder in the same change — use `/cleanup-old-code` in a separate step once nothing imports the old files
- Commit only when the user asks — follow the "Ask Before Committing" feedback rule
- If any step requires guessing (data source, endpoint, UI), STOP and ask the user — Rule 0 from CLAUDE.md