@codyswann/lisa 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +867 -0
- package/all/copy-overwrite/.claude/README.md +205 -0
- package/all/copy-overwrite/.claude/agents/agent-architect.md +311 -0
- package/all/copy-overwrite/.claude/agents/codebase-analyzer.md +146 -0
- package/all/copy-overwrite/.claude/agents/codebase-locator.md +125 -0
- package/all/copy-overwrite/.claude/agents/codebase-pattern-finder.md +237 -0
- package/all/copy-overwrite/.claude/agents/git-history-analyzer.md +183 -0
- package/all/copy-overwrite/.claude/agents/hooks-expert.md +74 -0
- package/all/copy-overwrite/.claude/agents/skill-evaluator.md +246 -0
- package/all/copy-overwrite/.claude/agents/slash-command-architect.md +87 -0
- package/all/copy-overwrite/.claude/agents/web-search-researcher.md +112 -0
- package/all/copy-overwrite/.claude/commands/git/commit-and-submit-pr.md +8 -0
- package/all/copy-overwrite/.claude/commands/git/commit.md +44 -0
- package/all/copy-overwrite/.claude/commands/git/prune.md +34 -0
- package/all/copy-overwrite/.claude/commands/git/submit-pr.md +50 -0
- package/all/copy-overwrite/.claude/commands/jira/create.md +50 -0
- package/all/copy-overwrite/.claude/commands/jira/verify.md +34 -0
- package/all/copy-overwrite/.claude/commands/project/archive.md +8 -0
- package/all/copy-overwrite/.claude/commands/project/bootstrap.md +49 -0
- package/all/copy-overwrite/.claude/commands/project/complete-task.md +7 -0
- package/all/copy-overwrite/.claude/commands/project/debrief.md +65 -0
- package/all/copy-overwrite/.claude/commands/project/execute.md +94 -0
- package/all/copy-overwrite/.claude/commands/project/implement.md +42 -0
- package/all/copy-overwrite/.claude/commands/project/local-code-review.md +88 -0
- package/all/copy-overwrite/.claude/commands/project/lower-code-complexity.md +74 -0
- package/all/copy-overwrite/.claude/commands/project/plan.md +314 -0
- package/all/copy-overwrite/.claude/commands/project/research.md +248 -0
- package/all/copy-overwrite/.claude/commands/project/review.md +63 -0
- package/all/copy-overwrite/.claude/commands/project/setup.md +19 -0
- package/all/copy-overwrite/.claude/commands/project/verify.md +38 -0
- package/all/copy-overwrite/.claude/commands/pull-request/review.md +12 -0
- package/all/copy-overwrite/.claude/commands/rules/format-md.md +72 -0
- package/all/copy-overwrite/.claude/commands/sonarqube/check.md +6 -0
- package/all/copy-overwrite/.claude/commands/sonarqube/fix.md +3 -0
- package/all/copy-overwrite/.claude/hooks/README.md +301 -0
- package/all/copy-overwrite/.claude/hooks/notify-ntfy.sh +181 -0
- package/all/copy-overwrite/.claude/settings.json +41 -0
- package/all/copy-overwrite/.claude/settings.local.json.example +14 -0
- package/all/copy-overwrite/.claude/skills/coding-philosophy/SKILL.md +405 -0
- package/all/copy-overwrite/.claude/skills/coding-philosophy/references/function-structure.md +416 -0
- package/all/copy-overwrite/.claude/skills/coding-philosophy/references/immutable-patterns.md +316 -0
- package/all/copy-overwrite/.claude/skills/prompt-complexity-scorer/SKILL.md +118 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/LICENSE.txt +202 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/SKILL.md +210 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/scripts/__pycache__/quick_validate.cpython-312.pyc +0 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/scripts/quick_validate.py +65 -0
- package/all/copy-overwrite/CLAUDE.md +77 -0
- package/all/copy-overwrite/HUMAN.md +17 -0
- package/all/copy-overwrite/specs/.keep +0 -0
- package/all/create-only/PROJECT_RULES.md +0 -0
- package/cdk/merge/package.json +20 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +107 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/prompts.d.ts +45 -0
- package/dist/cli/prompts.d.ts.map +1 -0
- package/dist/cli/prompts.js +58 -0
- package/dist/cli/prompts.js.map +1 -0
- package/dist/core/config.d.ts +73 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +36 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/lisa.d.ts +81 -0
- package/dist/core/lisa.d.ts.map +1 -0
- package/dist/core/lisa.js +459 -0
- package/dist/core/lisa.js.map +1 -0
- package/dist/core/manifest.d.ts +58 -0
- package/dist/core/manifest.d.ts.map +1 -0
- package/dist/core/manifest.js +104 -0
- package/dist/core/manifest.js.map +1 -0
- package/dist/detection/detector.interface.d.ts +15 -0
- package/dist/detection/detector.interface.d.ts.map +1 -0
- package/dist/detection/detector.interface.js +2 -0
- package/dist/detection/detector.interface.js.map +1 -0
- package/dist/detection/detectors/cdk.d.ts +10 -0
- package/dist/detection/detectors/cdk.d.ts.map +1 -0
- package/dist/detection/detectors/cdk.js +34 -0
- package/dist/detection/detectors/cdk.js.map +1 -0
- package/dist/detection/detectors/expo.d.ts +10 -0
- package/dist/detection/detectors/expo.d.ts.map +1 -0
- package/dist/detection/detectors/expo.js +30 -0
- package/dist/detection/detectors/expo.js.map +1 -0
- package/dist/detection/detectors/nestjs.d.ts +10 -0
- package/dist/detection/detectors/nestjs.d.ts.map +1 -0
- package/dist/detection/detectors/nestjs.js +34 -0
- package/dist/detection/detectors/nestjs.js.map +1 -0
- package/dist/detection/detectors/npm-package.d.ts +13 -0
- package/dist/detection/detectors/npm-package.d.ts.map +1 -0
- package/dist/detection/detectors/npm-package.js +30 -0
- package/dist/detection/detectors/npm-package.js.map +1 -0
- package/dist/detection/detectors/typescript.d.ts +10 -0
- package/dist/detection/detectors/typescript.d.ts.map +1 -0
- package/dist/detection/detectors/typescript.js +25 -0
- package/dist/detection/detectors/typescript.js.map +1 -0
- package/dist/detection/index.d.ts +24 -0
- package/dist/detection/index.d.ts.map +1 -0
- package/dist/detection/index.js +57 -0
- package/dist/detection/index.js.map +1 -0
- package/dist/errors/index.d.ts +69 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +110 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/console-logger.d.ts +12 -0
- package/dist/logging/console-logger.d.ts.map +1 -0
- package/dist/logging/console-logger.js +22 -0
- package/dist/logging/console-logger.js.map +1 -0
- package/dist/logging/index.d.ts +4 -0
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/logging/index.js +3 -0
- package/dist/logging/index.js.map +1 -0
- package/dist/logging/logger.interface.d.ts +20 -0
- package/dist/logging/logger.interface.d.ts.map +1 -0
- package/dist/logging/logger.interface.js +2 -0
- package/dist/logging/logger.interface.js.map +1 -0
- package/dist/logging/silent-logger.d.ts +12 -0
- package/dist/logging/silent-logger.d.ts.map +1 -0
- package/dist/logging/silent-logger.js +21 -0
- package/dist/logging/silent-logger.js.map +1 -0
- package/dist/strategies/copy-contents.d.ts +14 -0
- package/dist/strategies/copy-contents.d.ts.map +1 -0
- package/dist/strategies/copy-contents.js +69 -0
- package/dist/strategies/copy-contents.js.map +1 -0
- package/dist/strategies/copy-overwrite.d.ts +14 -0
- package/dist/strategies/copy-overwrite.d.ts.map +1 -0
- package/dist/strategies/copy-overwrite.js +47 -0
- package/dist/strategies/copy-overwrite.js.map +1 -0
- package/dist/strategies/create-only.d.ts +13 -0
- package/dist/strategies/create-only.d.ts.map +1 -0
- package/dist/strategies/create-only.js +30 -0
- package/dist/strategies/create-only.js.map +1 -0
- package/dist/strategies/index.d.ts +31 -0
- package/dist/strategies/index.d.ts.map +1 -0
- package/dist/strategies/index.js +52 -0
- package/dist/strategies/index.js.map +1 -0
- package/dist/strategies/merge.d.ts +13 -0
- package/dist/strategies/merge.d.ts.map +1 -0
- package/dist/strategies/merge.js +60 -0
- package/dist/strategies/merge.js.map +1 -0
- package/dist/strategies/strategy.interface.d.ts +31 -0
- package/dist/strategies/strategy.interface.d.ts.map +1 -0
- package/dist/strategies/strategy.interface.js +2 -0
- package/dist/strategies/strategy.interface.js.map +1 -0
- package/dist/transaction/backup.d.ts +38 -0
- package/dist/transaction/backup.d.ts.map +1 -0
- package/dist/transaction/backup.js +97 -0
- package/dist/transaction/backup.js.map +1 -0
- package/dist/transaction/index.d.ts +4 -0
- package/dist/transaction/index.d.ts.map +1 -0
- package/dist/transaction/index.js +3 -0
- package/dist/transaction/index.js.map +1 -0
- package/dist/transaction/transaction.d.ts +34 -0
- package/dist/transaction/transaction.d.ts.map +1 -0
- package/dist/transaction/transaction.js +68 -0
- package/dist/transaction/transaction.js.map +1 -0
- package/dist/utils/file-operations.d.ts +29 -0
- package/dist/utils/file-operations.d.ts.map +1 -0
- package/dist/utils/file-operations.js +84 -0
- package/dist/utils/file-operations.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/json-utils.d.ts +22 -0
- package/dist/utils/json-utils.d.ts.map +1 -0
- package/dist/utils/json-utils.js +57 -0
- package/dist/utils/json-utils.js.map +1 -0
- package/dist/utils/path-utils.d.ts +21 -0
- package/dist/utils/path-utils.d.ts.map +1 -0
- package/dist/utils/path-utils.js +35 -0
- package/dist/utils/path-utils.js.map +1 -0
- package/eslint-plugin-code-organization/README.md +149 -0
- package/eslint-plugin-code-organization/__tests__/enforce-statement-order.test.js +468 -0
- package/eslint-plugin-code-organization/index.js +23 -0
- package/eslint-plugin-code-organization/package.json +10 -0
- package/eslint-plugin-code-organization/rules/enforce-statement-order.js +157 -0
- package/expo/copy-overwrite/.claude/skills/apollo-client/SKILL.md +238 -0
- package/expo/copy-overwrite/.claude/skills/apollo-client/references/mutation-patterns.md +360 -0
- package/expo/copy-overwrite/.claude/skills/atomic-design-gluestack/SKILL.md +360 -0
- package/expo/copy-overwrite/.claude/skills/atomic-design-gluestack/references/atomic-levels.md +417 -0
- package/expo/copy-overwrite/.claude/skills/atomic-design-gluestack/references/folder-structure.md +257 -0
- package/expo/copy-overwrite/.claude/skills/atomic-design-gluestack/references/gluestack-mapping.md +233 -0
- package/expo/copy-overwrite/.claude/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +327 -0
- package/expo/copy-overwrite/.claude/skills/container-view-pattern/SKILL.md +299 -0
- package/expo/copy-overwrite/.claude/skills/container-view-pattern/references/examples.md +749 -0
- package/expo/copy-overwrite/.claude/skills/container-view-pattern/references/patterns.md +318 -0
- package/expo/copy-overwrite/.claude/skills/container-view-pattern/scripts/create_component.py +198 -0
- package/expo/copy-overwrite/.claude/skills/container-view-pattern/scripts/validate_component.py +207 -0
- package/expo/copy-overwrite/.claude/skills/cross-platform-compatibility/SKILL.md +268 -0
- package/expo/copy-overwrite/.claude/skills/cross-platform-compatibility/references/common-issues.md +619 -0
- package/expo/copy-overwrite/.claude/skills/cross-platform-compatibility/references/file-extensions.md +340 -0
- package/expo/copy-overwrite/.claude/skills/cross-platform-compatibility/references/platform-api.md +276 -0
- package/expo/copy-overwrite/.claude/skills/cross-platform-compatibility/scripts/validate_cross_platform.py +414 -0
- package/expo/copy-overwrite/.claude/skills/directory-structure/SKILL.md +202 -0
- package/expo/copy-overwrite/.claude/skills/directory-structure/scripts/validate_structure.py +443 -0
- package/expo/copy-overwrite/.claude/skills/expo-env-config/SKILL.md +309 -0
- package/expo/copy-overwrite/.claude/skills/expo-env-config/references/validation-patterns.md +417 -0
- package/expo/copy-overwrite/.claude/skills/expo-router-best-practices/SKILL.md +431 -0
- package/expo/copy-overwrite/.claude/skills/expo-router-best-practices/references/official-docs.md +290 -0
- package/expo/copy-overwrite/.claude/skills/expo-router-best-practices/scripts/generate-route.py +169 -0
- package/expo/copy-overwrite/.claude/skills/gluestack-nativewind/SKILL.md +411 -0
- package/expo/copy-overwrite/.claude/skills/gluestack-nativewind/references/color-tokens.md +343 -0
- package/expo/copy-overwrite/.claude/skills/gluestack-nativewind/references/component-mapping.md +307 -0
- package/expo/copy-overwrite/.claude/skills/gluestack-nativewind/references/spacing-scale.md +300 -0
- package/expo/copy-overwrite/.claude/skills/gluestack-nativewind/scripts/validate_styling.py +354 -0
- package/expo/copy-overwrite/.claude/skills/local-state/SKILL.md +362 -0
- package/expo/copy-overwrite/.claude/skills/local-state/references/async-storage.md +505 -0
- package/expo/copy-overwrite/.claude/skills/local-state/references/persistence-patterns.md +711 -0
- package/expo/copy-overwrite/.claude/skills/local-state/references/reactive-variables.md +446 -0
- package/expo/copy-overwrite/.claude/skills/playwright-selectors/SKILL.md +223 -0
- package/expo/copy-overwrite/.claude/skills/testing-library/SKILL.md +319 -0
- package/expo/copy-overwrite/.claude/skills/testing-library/references/async-patterns.md +420 -0
- package/expo/copy-overwrite/.claude/skills/testing-library/references/expo-router-testing.md +556 -0
- package/expo/copy-overwrite/.claude/skills/testing-library/references/mocking-patterns.md +590 -0
- package/expo/copy-overwrite/.claude/skills/testing-library/references/query-priority.md +291 -0
- package/expo/copy-overwrite/.easignore.extra +2 -0
- package/expo/copy-overwrite/.mcp.json +33 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/README.md +234 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/__tests__/plugin-index.test.js +84 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/__tests__/require-memo-in-view.test.js +196 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/__tests__/single-component-per-file.test.js +289 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/index.js +32 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/package.json +10 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/rules/enforce-component-structure.js +230 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/rules/no-return-in-view.js +91 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/rules/require-memo-in-view.js +178 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/rules/single-component-per-file.js +238 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/README.md +260 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/index.js +29 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/package.json +10 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/rules/no-classname-outside-ui.js +51 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/rules/no-direct-rn-imports.js +55 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/rules/no-inline-styles.js +73 -0
- package/expo/copy-overwrite/eslint.config.mjs +560 -0
- package/expo/copy-overwrite/lighthouserc.js +194 -0
- package/expo/create-only/lighthouserc-config.json +28 -0
- package/expo/merge/package.json +132 -0
- package/lisa.sh +35 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/SKILL.md +176 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/references/advanced-features.md +527 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/references/project-patterns.md +483 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/references/quick-start.md +257 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/references/resolvers-mutations.md +413 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/references/types-scalars.md +513 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-rules/SKILL.md +536 -0
- package/nestjs/copy-overwrite/.claude/skills/typeorm-patterns/SKILL.md +275 -0
- package/nestjs/copy-overwrite/.claude/skills/typeorm-patterns/references/configuration-patterns.md +487 -0
- package/nestjs/copy-overwrite/.claude/skills/typeorm-patterns/references/entity-patterns.md +450 -0
- package/nestjs/copy-overwrite/.claude/skills/typeorm-patterns/references/observability-patterns.md +536 -0
- package/nestjs/merge/package.json +75 -0
- package/package.json +124 -0
- package/typescript/copy-contents/.husky/commit-msg +91 -0
- package/typescript/copy-contents/.husky/pre-commit +96 -0
- package/typescript/copy-contents/.husky/pre-push +211 -0
- package/typescript/copy-overwrite/.claude/hooks/format-on-edit.sh +74 -0
- package/typescript/copy-overwrite/.claude/hooks/install_pkgs.sh +59 -0
- package/typescript/copy-overwrite/.claude/hooks/lint-on-edit.sh +103 -0
- package/typescript/copy-overwrite/.claude/skills/jsdoc-best-practices/SKILL.md +388 -0
- package/typescript/copy-overwrite/.github/README.md +455 -0
- package/typescript/copy-overwrite/.github/dependabot.yml +40 -0
- package/typescript/copy-overwrite/.github/k6/BROWSER_TESTING_NOTE.md +129 -0
- package/typescript/copy-overwrite/.github/k6/INTEGRATION_GUIDE.md +354 -0
- package/typescript/copy-overwrite/.github/k6/README.md +386 -0
- package/typescript/copy-overwrite/.github/k6/SCENARIO_SELECTION_GUIDE.md +264 -0
- package/typescript/copy-overwrite/.github/k6/examples/customer-deploy-integration.yml +115 -0
- package/typescript/copy-overwrite/.github/k6/examples/data-driven-test.js +268 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/load.js +142 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/load.json +27 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/smoke.js +26 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/smoke.json +20 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/soak.js +244 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/soak.json +29 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/spike.js +180 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/spike.json +32 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/stress.js +206 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/stress.json +38 -0
- package/typescript/copy-overwrite/.github/k6/scripts/api-test.js +452 -0
- package/typescript/copy-overwrite/.github/k6/scripts/default-test.js +185 -0
- package/typescript/copy-overwrite/.github/k6/thresholds/normal.json +30 -0
- package/typescript/copy-overwrite/.github/k6/thresholds/relaxed.json +21 -0
- package/typescript/copy-overwrite/.github/k6/thresholds/strict.json +29 -0
- package/typescript/copy-overwrite/.github/workflows/build.yml +72 -0
- package/typescript/copy-overwrite/.github/workflows/ci.yml +49 -0
- package/typescript/copy-overwrite/.github/workflows/claude.yml +51 -0
- package/typescript/copy-overwrite/.github/workflows/create-github-issue-on-failure.yml +113 -0
- package/typescript/copy-overwrite/.github/workflows/create-jira-issue-on-failure.yml +195 -0
- package/typescript/copy-overwrite/.github/workflows/create-sentry-issue-on-failure.yml +267 -0
- package/typescript/copy-overwrite/.github/workflows/deploy.yml +228 -0
- package/typescript/copy-overwrite/.github/workflows/k6-load-test-README.md +230 -0
- package/typescript/copy-overwrite/.github/workflows/lighthouse.yml +68 -0
- package/typescript/copy-overwrite/.github/workflows/load-test.yml +282 -0
- package/typescript/copy-overwrite/.github/workflows/quality.yml +1737 -0
- package/typescript/copy-overwrite/.github/workflows/release.yml +1599 -0
- package/typescript/copy-overwrite/.gitleaksignore +28 -0
- package/typescript/copy-overwrite/.nvmrc +1 -0
- package/typescript/copy-overwrite/.prettierignore +23 -0
- package/typescript/copy-overwrite/.prettierrc.json +22 -0
- package/typescript/copy-overwrite/.versionrc +42 -0
- package/typescript/copy-overwrite/.yamllint +20 -0
- package/typescript/copy-overwrite/commitlint.config.js +11 -0
- package/typescript/copy-overwrite/eslint-plugin-code-organization/README.md +149 -0
- package/typescript/copy-overwrite/eslint-plugin-code-organization/__tests__/enforce-statement-order.test.js +468 -0
- package/typescript/copy-overwrite/eslint-plugin-code-organization/index.js +23 -0
- package/typescript/copy-overwrite/eslint-plugin-code-organization/package.json +10 -0
- package/typescript/copy-overwrite/eslint-plugin-code-organization/rules/enforce-statement-order.js +157 -0
- package/typescript/copy-overwrite/eslint.config.mjs +390 -0
- package/typescript/copy-overwrite/eslint.ignore.config.json +57 -0
- package/typescript/copy-overwrite/eslint.thresholds.config.json +5 -0
- package/typescript/github-rulesets/base.json +106 -0
- package/typescript/merge/.claude/settings.json +28 -0
- package/typescript/merge/package.json +71 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
# Container/View Pattern - Complete Examples
|
|
2
|
+
|
|
3
|
+
## Example 1: Simple Button Component
|
|
4
|
+
|
|
5
|
+
### Directory Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
AddButton/
|
|
9
|
+
├── AddButtonContainer.tsx
|
|
10
|
+
├── AddButtonView.tsx
|
|
11
|
+
└── index.tsx
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### AddButtonContainer.tsx
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { useCallback } from "react";
|
|
18
|
+
|
|
19
|
+
import AddButtonView from "./AddButtonView";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Props for the AddButton component.
|
|
23
|
+
*/
|
|
24
|
+
interface AddButtonProps {
|
|
25
|
+
/** Callback when button is pressed */
|
|
26
|
+
readonly onAdd: () => void;
|
|
27
|
+
/** Whether the button is disabled */
|
|
28
|
+
readonly isDisabled?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Container component that manages the add button logic.
|
|
33
|
+
* @param props - Component properties
|
|
34
|
+
* @param props.onAdd - Callback when button is pressed
|
|
35
|
+
* @param props.isDisabled - Whether the button is disabled
|
|
36
|
+
*/
|
|
37
|
+
const AddButtonContainer = ({ onAdd, isDisabled = false }: AddButtonProps) => {
|
|
38
|
+
const handlePress = useCallback(() => {
|
|
39
|
+
if (!isDisabled) {
|
|
40
|
+
onAdd();
|
|
41
|
+
}
|
|
42
|
+
}, [onAdd, isDisabled]);
|
|
43
|
+
|
|
44
|
+
return <AddButtonView onPress={handlePress} isDisabled={isDisabled} />;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default AddButtonContainer;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### AddButtonView.tsx
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import { memo } from "react";
|
|
54
|
+
import { Plus } from "lucide-react-native";
|
|
55
|
+
|
|
56
|
+
import { Button, ButtonIcon, ButtonText } from "@/components/ui/button";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Props for the AddButtonView component.
|
|
60
|
+
*/
|
|
61
|
+
interface AddButtonViewProps {
|
|
62
|
+
/** Handler for button press */
|
|
63
|
+
readonly onPress: () => void;
|
|
64
|
+
/** Whether the button is disabled */
|
|
65
|
+
readonly isDisabled: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* View component that renders the add button UI.
|
|
70
|
+
* @param props - Component properties
|
|
71
|
+
* @param props.onPress - Handler for button press
|
|
72
|
+
* @param props.isDisabled - Whether the button is disabled
|
|
73
|
+
*/
|
|
74
|
+
const AddButtonView = ({ onPress, isDisabled }: AddButtonViewProps) => (
|
|
75
|
+
<Button
|
|
76
|
+
testID="ADD_BUTTON.BUTTON"
|
|
77
|
+
onPress={onPress}
|
|
78
|
+
isDisabled={isDisabled}
|
|
79
|
+
className="flex-row items-center gap-2"
|
|
80
|
+
>
|
|
81
|
+
<ButtonIcon as={Plus} />
|
|
82
|
+
<ButtonText>Add</ButtonText>
|
|
83
|
+
</Button>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
AddButtonView.displayName = "AddButtonView";
|
|
87
|
+
|
|
88
|
+
export default memo(AddButtonView);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### index.tsx
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
export { default } from "./AddButtonContainer";
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Example 2: List with Loading/Empty States
|
|
98
|
+
|
|
99
|
+
### Directory Structure
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
PlayerList/
|
|
103
|
+
├── PlayerListContainer.tsx
|
|
104
|
+
├── PlayerListView.tsx
|
|
105
|
+
└── index.tsx
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### PlayerListContainer.tsx
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import { useCallback, useMemo } from "react";
|
|
112
|
+
import { useRouter } from "expo-router";
|
|
113
|
+
|
|
114
|
+
import { useListPlayersQuery } from "@/generated/graphql";
|
|
115
|
+
import PlayerListView from "./PlayerListView";
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Props for the PlayerList component.
|
|
119
|
+
*/
|
|
120
|
+
interface PlayerListProps {
|
|
121
|
+
/** Optional filter for player position */
|
|
122
|
+
readonly positionFilter?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Container component that manages player list data and navigation.
|
|
127
|
+
* @param props - Component properties
|
|
128
|
+
* @param props.positionFilter - Optional filter for player position
|
|
129
|
+
*/
|
|
130
|
+
const PlayerListContainer = ({ positionFilter }: PlayerListProps) => {
|
|
131
|
+
const router = useRouter();
|
|
132
|
+
const { data, loading, error, refetch } = useListPlayersQuery();
|
|
133
|
+
|
|
134
|
+
const players = useMemo(() => {
|
|
135
|
+
const allPlayers = data?.listPlayers?.edges ?? [];
|
|
136
|
+
if (!positionFilter) {
|
|
137
|
+
return allPlayers;
|
|
138
|
+
}
|
|
139
|
+
return allPlayers.filter(p => p.position === positionFilter);
|
|
140
|
+
}, [data?.listPlayers?.edges, positionFilter]);
|
|
141
|
+
|
|
142
|
+
const isEmpty = useMemo(
|
|
143
|
+
() => !loading && !error && players.length === 0,
|
|
144
|
+
[loading, error, players.length]
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const handlePlayerPress = useCallback(
|
|
148
|
+
(playerId: string) => {
|
|
149
|
+
if (!playerId) {
|
|
150
|
+
console.error("Cannot navigate: player ID is missing");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
router.push(`/players/${playerId}`);
|
|
154
|
+
},
|
|
155
|
+
[router]
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const handleRefresh = useCallback(() => {
|
|
159
|
+
refetch();
|
|
160
|
+
}, [refetch]);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<PlayerListView
|
|
164
|
+
players={players}
|
|
165
|
+
isLoading={loading}
|
|
166
|
+
hasError={!!error}
|
|
167
|
+
isEmpty={isEmpty}
|
|
168
|
+
onPlayerPress={handlePlayerPress}
|
|
169
|
+
onRefresh={handleRefresh}
|
|
170
|
+
/>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export default PlayerListContainer;
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### PlayerListView.tsx
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
import { memo } from "react";
|
|
181
|
+
import { FlashList, ListRenderItem } from "@shopify/flash-list";
|
|
182
|
+
|
|
183
|
+
import { Box } from "@/components/ui/box";
|
|
184
|
+
import { Text } from "@/components/ui/text";
|
|
185
|
+
import { Pressable } from "@/components/ui/pressable";
|
|
186
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
187
|
+
import { PlayerFragment } from "@/generated/graphql";
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Props for the PlayerListView component.
|
|
191
|
+
*/
|
|
192
|
+
interface PlayerListViewProps {
|
|
193
|
+
/** List of players to display */
|
|
194
|
+
readonly players: readonly PlayerFragment[];
|
|
195
|
+
/** Whether the list is loading */
|
|
196
|
+
readonly isLoading: boolean;
|
|
197
|
+
/** Whether there was an error loading */
|
|
198
|
+
readonly hasError: boolean;
|
|
199
|
+
/** Whether the list is empty */
|
|
200
|
+
readonly isEmpty: boolean;
|
|
201
|
+
/** Handler for player item press */
|
|
202
|
+
readonly onPlayerPress: (playerId: string) => void;
|
|
203
|
+
/** Handler for pull-to-refresh */
|
|
204
|
+
readonly onRefresh: () => void;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Renders a single player item.
|
|
209
|
+
* @param props - Render item props
|
|
210
|
+
* @param props.player - The player data
|
|
211
|
+
* @param props.onPress - Press handler
|
|
212
|
+
*/
|
|
213
|
+
function renderPlayerItem(props: {
|
|
214
|
+
readonly player: PlayerFragment;
|
|
215
|
+
readonly onPress: (id: string) => void;
|
|
216
|
+
}) {
|
|
217
|
+
const { player, onPress } = props;
|
|
218
|
+
return (
|
|
219
|
+
<Pressable
|
|
220
|
+
testID={`PLAYER_LIST.ITEM.${player.id}`}
|
|
221
|
+
onPress={() => onPress(player.id)}
|
|
222
|
+
className="flex-row items-center gap-4 p-4"
|
|
223
|
+
>
|
|
224
|
+
<Text className="text-lg font-medium">{player.name}</Text>
|
|
225
|
+
<Text className="text-sm text-gray-500">{player.position}</Text>
|
|
226
|
+
</Pressable>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Renders the loading state.
|
|
232
|
+
*/
|
|
233
|
+
function renderLoading() {
|
|
234
|
+
return (
|
|
235
|
+
<Box className="flex-1 items-center justify-center">
|
|
236
|
+
<Spinner size="large" />
|
|
237
|
+
</Box>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Renders the error state.
|
|
243
|
+
*/
|
|
244
|
+
function renderError() {
|
|
245
|
+
return (
|
|
246
|
+
<Box className="flex-1 items-center justify-center p-8">
|
|
247
|
+
<Text className="text-center text-red-500">
|
|
248
|
+
Failed to load players. Pull to refresh.
|
|
249
|
+
</Text>
|
|
250
|
+
</Box>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Renders the empty state.
|
|
256
|
+
*/
|
|
257
|
+
function renderEmpty() {
|
|
258
|
+
return (
|
|
259
|
+
<Box className="flex-1 items-center justify-center p-8">
|
|
260
|
+
<Text className="text-center text-gray-500">No players found</Text>
|
|
261
|
+
</Box>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* View component that renders the player list UI.
|
|
267
|
+
* @param props - Component properties
|
|
268
|
+
* @param props.players - List of players to display
|
|
269
|
+
* @param props.isLoading - Whether the list is loading
|
|
270
|
+
* @param props.hasError - Whether there was an error loading
|
|
271
|
+
* @param props.isEmpty - Whether the list is empty
|
|
272
|
+
* @param props.onPlayerPress - Handler for player item press
|
|
273
|
+
* @param props.onRefresh - Handler for pull-to-refresh
|
|
274
|
+
*/
|
|
275
|
+
const PlayerListView = ({
|
|
276
|
+
players,
|
|
277
|
+
isLoading,
|
|
278
|
+
hasError,
|
|
279
|
+
isEmpty,
|
|
280
|
+
onPlayerPress,
|
|
281
|
+
onRefresh,
|
|
282
|
+
}: PlayerListViewProps) => (
|
|
283
|
+
<Box testID="PLAYER_LIST.CONTAINER" className="flex-1">
|
|
284
|
+
{isLoading ? (
|
|
285
|
+
renderLoading()
|
|
286
|
+
) : hasError ? (
|
|
287
|
+
renderError()
|
|
288
|
+
) : isEmpty ? (
|
|
289
|
+
renderEmpty()
|
|
290
|
+
) : (
|
|
291
|
+
<FlashList
|
|
292
|
+
data={players}
|
|
293
|
+
renderItem={({ item }) =>
|
|
294
|
+
renderPlayerItem({ player: item, onPress: onPlayerPress })
|
|
295
|
+
}
|
|
296
|
+
estimatedItemSize={72}
|
|
297
|
+
onRefresh={onRefresh}
|
|
298
|
+
refreshing={isLoading}
|
|
299
|
+
/>
|
|
300
|
+
)}
|
|
301
|
+
</Box>
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
PlayerListView.displayName = "PlayerListView";
|
|
305
|
+
|
|
306
|
+
export default memo(PlayerListView);
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### index.tsx
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
export { default } from "./PlayerListContainer";
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Example 3: Form with Validation
|
|
316
|
+
|
|
317
|
+
### Directory Structure
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
EditProfile/
|
|
321
|
+
├── EditProfileContainer.tsx
|
|
322
|
+
├── EditProfileView.tsx
|
|
323
|
+
└── index.tsx
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### EditProfileContainer.tsx
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
import { useCallback, useMemo, useState } from "react";
|
|
330
|
+
import { useForm } from "react-hook-form";
|
|
331
|
+
import { yupResolver } from "@hookform/resolvers/yup";
|
|
332
|
+
import * as yup from "yup";
|
|
333
|
+
|
|
334
|
+
import { useUpdateProfileMutation } from "@/generated/graphql";
|
|
335
|
+
import EditProfileView from "./EditProfileView";
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Form data shape for profile editing.
|
|
339
|
+
*/
|
|
340
|
+
interface ProfileFormData {
|
|
341
|
+
readonly name: string;
|
|
342
|
+
readonly email: string;
|
|
343
|
+
readonly bio?: string;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const schema = yup.object({
|
|
347
|
+
name: yup.string().required("Name is required"),
|
|
348
|
+
email: yup.string().email("Invalid email").required("Email is required"),
|
|
349
|
+
bio: yup.string().max(500, "Bio must be 500 characters or less"),
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Props for the EditProfile component.
|
|
354
|
+
*/
|
|
355
|
+
interface EditProfileProps {
|
|
356
|
+
/** Initial profile data */
|
|
357
|
+
readonly initialData: ProfileFormData;
|
|
358
|
+
/** Callback when profile is saved */
|
|
359
|
+
readonly onSaved: () => void;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Container component that manages profile editing logic.
|
|
364
|
+
* @param props - Component properties
|
|
365
|
+
* @param props.initialData - Initial profile data
|
|
366
|
+
* @param props.onSaved - Callback when profile is saved
|
|
367
|
+
*/
|
|
368
|
+
const EditProfileContainer = ({ initialData, onSaved }: EditProfileProps) => {
|
|
369
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
370
|
+
const [updateProfile] = useUpdateProfileMutation();
|
|
371
|
+
|
|
372
|
+
const {
|
|
373
|
+
control,
|
|
374
|
+
handleSubmit,
|
|
375
|
+
formState: { errors, isDirty },
|
|
376
|
+
} = useForm<ProfileFormData>({
|
|
377
|
+
resolver: yupResolver(schema),
|
|
378
|
+
defaultValues: initialData,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const errorMessages = useMemo(
|
|
382
|
+
() => ({
|
|
383
|
+
name: errors.name?.message,
|
|
384
|
+
email: errors.email?.message,
|
|
385
|
+
bio: errors.bio?.message,
|
|
386
|
+
}),
|
|
387
|
+
[errors.name?.message, errors.email?.message, errors.bio?.message]
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const handleFormSubmit = useCallback(
|
|
391
|
+
async (data: ProfileFormData) => {
|
|
392
|
+
setIsSubmitting(true);
|
|
393
|
+
try {
|
|
394
|
+
await updateProfile({ variables: { input: data } });
|
|
395
|
+
onSaved();
|
|
396
|
+
} catch (error) {
|
|
397
|
+
console.error("Failed to update profile:", error);
|
|
398
|
+
} finally {
|
|
399
|
+
setIsSubmitting(false);
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
[updateProfile, onSaved]
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
const onSubmit = useMemo(
|
|
406
|
+
() => handleSubmit(handleFormSubmit),
|
|
407
|
+
[handleSubmit, handleFormSubmit]
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
<EditProfileView
|
|
412
|
+
control={control}
|
|
413
|
+
errors={errorMessages}
|
|
414
|
+
isSubmitting={isSubmitting}
|
|
415
|
+
isDirty={isDirty}
|
|
416
|
+
onSubmit={onSubmit}
|
|
417
|
+
/>
|
|
418
|
+
);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
export default EditProfileContainer;
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### EditProfileView.tsx
|
|
425
|
+
|
|
426
|
+
```tsx
|
|
427
|
+
import { memo } from "react";
|
|
428
|
+
import { Control, Controller } from "react-hook-form";
|
|
429
|
+
|
|
430
|
+
import { Box } from "@/components/ui/box";
|
|
431
|
+
import { VStack } from "@/components/ui/vstack";
|
|
432
|
+
import { Text } from "@/components/ui/text";
|
|
433
|
+
import { Input, InputField } from "@/components/ui/input";
|
|
434
|
+
import { Textarea, TextareaInput } from "@/components/ui/textarea";
|
|
435
|
+
import { Button, ButtonText, ButtonSpinner } from "@/components/ui/button";
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Error messages for form fields.
|
|
439
|
+
*/
|
|
440
|
+
interface FormErrors {
|
|
441
|
+
readonly name?: string;
|
|
442
|
+
readonly email?: string;
|
|
443
|
+
readonly bio?: string;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Props for the EditProfileView component.
|
|
448
|
+
*/
|
|
449
|
+
interface EditProfileViewProps {
|
|
450
|
+
/** React Hook Form control object */
|
|
451
|
+
readonly control: Control<any>;
|
|
452
|
+
/** Validation error messages */
|
|
453
|
+
readonly errors: FormErrors;
|
|
454
|
+
/** Whether the form is submitting */
|
|
455
|
+
readonly isSubmitting: boolean;
|
|
456
|
+
/** Whether the form has changes */
|
|
457
|
+
readonly isDirty: boolean;
|
|
458
|
+
/** Handler for form submission */
|
|
459
|
+
readonly onSubmit: () => void;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* View component that renders the profile edit form UI.
|
|
464
|
+
* @param props - Component properties
|
|
465
|
+
* @param props.control - React Hook Form control object
|
|
466
|
+
* @param props.errors - Validation error messages
|
|
467
|
+
* @param props.isSubmitting - Whether the form is submitting
|
|
468
|
+
* @param props.isDirty - Whether the form has changes
|
|
469
|
+
* @param props.onSubmit - Handler for form submission
|
|
470
|
+
*/
|
|
471
|
+
const EditProfileView = ({
|
|
472
|
+
control,
|
|
473
|
+
errors,
|
|
474
|
+
isSubmitting,
|
|
475
|
+
isDirty,
|
|
476
|
+
onSubmit,
|
|
477
|
+
}: EditProfileViewProps) => (
|
|
478
|
+
<Box testID="EDIT_PROFILE.CONTAINER" className="flex-1 p-4">
|
|
479
|
+
<VStack space="lg">
|
|
480
|
+
<VStack space="xs">
|
|
481
|
+
<Text className="font-medium">Name</Text>
|
|
482
|
+
<Controller
|
|
483
|
+
control={control}
|
|
484
|
+
name="name"
|
|
485
|
+
render={({ field: { onChange, onBlur, value } }) => (
|
|
486
|
+
<Input isInvalid={!!errors.name}>
|
|
487
|
+
<InputField
|
|
488
|
+
testID="EDIT_PROFILE.NAME_INPUT"
|
|
489
|
+
placeholder="Enter your name"
|
|
490
|
+
value={value}
|
|
491
|
+
onChangeText={onChange}
|
|
492
|
+
onBlur={onBlur}
|
|
493
|
+
/>
|
|
494
|
+
</Input>
|
|
495
|
+
)}
|
|
496
|
+
/>
|
|
497
|
+
{errors.name && (
|
|
498
|
+
<Text className="text-sm text-red-500">{errors.name}</Text>
|
|
499
|
+
)}
|
|
500
|
+
</VStack>
|
|
501
|
+
|
|
502
|
+
<VStack space="xs">
|
|
503
|
+
<Text className="font-medium">Email</Text>
|
|
504
|
+
<Controller
|
|
505
|
+
control={control}
|
|
506
|
+
name="email"
|
|
507
|
+
render={({ field: { onChange, onBlur, value } }) => (
|
|
508
|
+
<Input isInvalid={!!errors.email}>
|
|
509
|
+
<InputField
|
|
510
|
+
testID="EDIT_PROFILE.EMAIL_INPUT"
|
|
511
|
+
placeholder="Enter your email"
|
|
512
|
+
keyboardType="email-address"
|
|
513
|
+
autoCapitalize="none"
|
|
514
|
+
value={value}
|
|
515
|
+
onChangeText={onChange}
|
|
516
|
+
onBlur={onBlur}
|
|
517
|
+
/>
|
|
518
|
+
</Input>
|
|
519
|
+
)}
|
|
520
|
+
/>
|
|
521
|
+
{errors.email && (
|
|
522
|
+
<Text className="text-sm text-red-500">{errors.email}</Text>
|
|
523
|
+
)}
|
|
524
|
+
</VStack>
|
|
525
|
+
|
|
526
|
+
<VStack space="xs">
|
|
527
|
+
<Text className="font-medium">Bio</Text>
|
|
528
|
+
<Controller
|
|
529
|
+
control={control}
|
|
530
|
+
name="bio"
|
|
531
|
+
render={({ field: { onChange, onBlur, value } }) => (
|
|
532
|
+
<Textarea isInvalid={!!errors.bio}>
|
|
533
|
+
<TextareaInput
|
|
534
|
+
testID="EDIT_PROFILE.BIO_INPUT"
|
|
535
|
+
placeholder="Tell us about yourself"
|
|
536
|
+
value={value}
|
|
537
|
+
onChangeText={onChange}
|
|
538
|
+
onBlur={onBlur}
|
|
539
|
+
/>
|
|
540
|
+
</Textarea>
|
|
541
|
+
)}
|
|
542
|
+
/>
|
|
543
|
+
{errors.bio && (
|
|
544
|
+
<Text className="text-sm text-red-500">{errors.bio}</Text>
|
|
545
|
+
)}
|
|
546
|
+
</VStack>
|
|
547
|
+
|
|
548
|
+
<Button
|
|
549
|
+
testID="EDIT_PROFILE.SUBMIT_BUTTON"
|
|
550
|
+
onPress={onSubmit}
|
|
551
|
+
isDisabled={!isDirty || isSubmitting}
|
|
552
|
+
>
|
|
553
|
+
{isSubmitting ? (
|
|
554
|
+
<ButtonSpinner />
|
|
555
|
+
) : (
|
|
556
|
+
<ButtonText>Save Changes</ButtonText>
|
|
557
|
+
)}
|
|
558
|
+
</Button>
|
|
559
|
+
</VStack>
|
|
560
|
+
</Box>
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
EditProfileView.displayName = "EditProfileView";
|
|
564
|
+
|
|
565
|
+
export default memo(EditProfileView);
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### index.tsx
|
|
569
|
+
|
|
570
|
+
```tsx
|
|
571
|
+
export { default } from "./EditProfileContainer";
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
## Example 4: Modal with Confirmation
|
|
575
|
+
|
|
576
|
+
### Directory Structure
|
|
577
|
+
|
|
578
|
+
```
|
|
579
|
+
DeleteConfirmModal/
|
|
580
|
+
├── DeleteConfirmModalContainer.tsx
|
|
581
|
+
├── DeleteConfirmModalView.tsx
|
|
582
|
+
└── index.tsx
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### DeleteConfirmModalContainer.tsx
|
|
586
|
+
|
|
587
|
+
```tsx
|
|
588
|
+
import { useCallback, useState } from "react";
|
|
589
|
+
|
|
590
|
+
import DeleteConfirmModalView from "./DeleteConfirmModalView";
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Props for the DeleteConfirmModal component.
|
|
594
|
+
*/
|
|
595
|
+
interface DeleteConfirmModalProps {
|
|
596
|
+
/** Whether the modal is visible */
|
|
597
|
+
readonly isOpen: boolean;
|
|
598
|
+
/** Name of the item being deleted */
|
|
599
|
+
readonly itemName: string;
|
|
600
|
+
/** Handler for delete confirmation */
|
|
601
|
+
readonly onConfirm: () => Promise<void>;
|
|
602
|
+
/** Handler for modal close */
|
|
603
|
+
readonly onClose: () => void;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Container component that manages delete confirmation modal logic.
|
|
608
|
+
* @param props - Component properties
|
|
609
|
+
* @param props.isOpen - Whether the modal is visible
|
|
610
|
+
* @param props.itemName - Name of the item being deleted
|
|
611
|
+
* @param props.onConfirm - Handler for delete confirmation
|
|
612
|
+
* @param props.onClose - Handler for modal close
|
|
613
|
+
*/
|
|
614
|
+
const DeleteConfirmModalContainer = ({
|
|
615
|
+
isOpen,
|
|
616
|
+
itemName,
|
|
617
|
+
onConfirm,
|
|
618
|
+
onClose,
|
|
619
|
+
}: DeleteConfirmModalProps) => {
|
|
620
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
621
|
+
|
|
622
|
+
const handleConfirm = useCallback(async () => {
|
|
623
|
+
setIsDeleting(true);
|
|
624
|
+
try {
|
|
625
|
+
await onConfirm();
|
|
626
|
+
onClose();
|
|
627
|
+
} catch (error) {
|
|
628
|
+
console.error("Delete failed:", error);
|
|
629
|
+
} finally {
|
|
630
|
+
setIsDeleting(false);
|
|
631
|
+
}
|
|
632
|
+
}, [onConfirm, onClose]);
|
|
633
|
+
|
|
634
|
+
const handleCancel = useCallback(() => {
|
|
635
|
+
if (!isDeleting) {
|
|
636
|
+
onClose();
|
|
637
|
+
}
|
|
638
|
+
}, [isDeleting, onClose]);
|
|
639
|
+
|
|
640
|
+
return (
|
|
641
|
+
<DeleteConfirmModalView
|
|
642
|
+
isOpen={isOpen}
|
|
643
|
+
itemName={itemName}
|
|
644
|
+
isDeleting={isDeleting}
|
|
645
|
+
onConfirm={handleConfirm}
|
|
646
|
+
onCancel={handleCancel}
|
|
647
|
+
/>
|
|
648
|
+
);
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
export default DeleteConfirmModalContainer;
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### DeleteConfirmModalView.tsx
|
|
655
|
+
|
|
656
|
+
```tsx
|
|
657
|
+
import { memo } from "react";
|
|
658
|
+
|
|
659
|
+
import {
|
|
660
|
+
AlertDialog,
|
|
661
|
+
AlertDialogBackdrop,
|
|
662
|
+
AlertDialogContent,
|
|
663
|
+
AlertDialogHeader,
|
|
664
|
+
AlertDialogBody,
|
|
665
|
+
AlertDialogFooter,
|
|
666
|
+
} from "@/components/ui/alert-dialog";
|
|
667
|
+
import { Heading } from "@/components/ui/heading";
|
|
668
|
+
import { Text } from "@/components/ui/text";
|
|
669
|
+
import { Button, ButtonText, ButtonSpinner } from "@/components/ui/button";
|
|
670
|
+
import { HStack } from "@/components/ui/hstack";
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Props for the DeleteConfirmModalView component.
|
|
674
|
+
*/
|
|
675
|
+
interface DeleteConfirmModalViewProps {
|
|
676
|
+
/** Whether the modal is visible */
|
|
677
|
+
readonly isOpen: boolean;
|
|
678
|
+
/** Name of the item being deleted */
|
|
679
|
+
readonly itemName: string;
|
|
680
|
+
/** Whether deletion is in progress */
|
|
681
|
+
readonly isDeleting: boolean;
|
|
682
|
+
/** Handler for confirm button */
|
|
683
|
+
readonly onConfirm: () => void;
|
|
684
|
+
/** Handler for cancel button */
|
|
685
|
+
readonly onCancel: () => void;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* View component that renders the delete confirmation modal UI.
|
|
690
|
+
* @param props - Component properties
|
|
691
|
+
* @param props.isOpen - Whether the modal is visible
|
|
692
|
+
* @param props.itemName - Name of the item being deleted
|
|
693
|
+
* @param props.isDeleting - Whether deletion is in progress
|
|
694
|
+
* @param props.onConfirm - Handler for confirm button
|
|
695
|
+
* @param props.onCancel - Handler for cancel button
|
|
696
|
+
*/
|
|
697
|
+
const DeleteConfirmModalView = ({
|
|
698
|
+
isOpen,
|
|
699
|
+
itemName,
|
|
700
|
+
isDeleting,
|
|
701
|
+
onConfirm,
|
|
702
|
+
onCancel,
|
|
703
|
+
}: DeleteConfirmModalViewProps) => (
|
|
704
|
+
<AlertDialog isOpen={isOpen} onClose={onCancel}>
|
|
705
|
+
<AlertDialogBackdrop />
|
|
706
|
+
<AlertDialogContent testID="DELETE_CONFIRM_MODAL.CONTENT">
|
|
707
|
+
<AlertDialogHeader>
|
|
708
|
+
<Heading size="lg">Delete {itemName}?</Heading>
|
|
709
|
+
</AlertDialogHeader>
|
|
710
|
+
<AlertDialogBody>
|
|
711
|
+
<Text>
|
|
712
|
+
This action cannot be undone. Are you sure you want to delete{" "}
|
|
713
|
+
<Text className="font-bold">{itemName}</Text>?
|
|
714
|
+
</Text>
|
|
715
|
+
</AlertDialogBody>
|
|
716
|
+
<AlertDialogFooter>
|
|
717
|
+
<HStack space="md">
|
|
718
|
+
<Button
|
|
719
|
+
testID="DELETE_CONFIRM_MODAL.CANCEL_BUTTON"
|
|
720
|
+
variant="outline"
|
|
721
|
+
onPress={onCancel}
|
|
722
|
+
isDisabled={isDeleting}
|
|
723
|
+
>
|
|
724
|
+
<ButtonText>Cancel</ButtonText>
|
|
725
|
+
</Button>
|
|
726
|
+
<Button
|
|
727
|
+
testID="DELETE_CONFIRM_MODAL.CONFIRM_BUTTON"
|
|
728
|
+
action="negative"
|
|
729
|
+
onPress={onConfirm}
|
|
730
|
+
isDisabled={isDeleting}
|
|
731
|
+
>
|
|
732
|
+
{isDeleting ? <ButtonSpinner /> : <ButtonText>Delete</ButtonText>}
|
|
733
|
+
</Button>
|
|
734
|
+
</HStack>
|
|
735
|
+
</AlertDialogFooter>
|
|
736
|
+
</AlertDialogContent>
|
|
737
|
+
</AlertDialog>
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
DeleteConfirmModalView.displayName = "DeleteConfirmModalView";
|
|
741
|
+
|
|
742
|
+
export default memo(DeleteConfirmModalView);
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### index.tsx
|
|
746
|
+
|
|
747
|
+
```tsx
|
|
748
|
+
export { default } from "./DeleteConfirmModalContainer";
|
|
749
|
+
```
|