@rashidee/co2 1.2.5

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.
Files changed (190) hide show
  1. package/dist/index.js +25449 -0
  2. package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
  3. package/drizzle/0001_wise_wolf_cub.sql +13 -0
  4. package/drizzle/0002_secret_spyke.sql +16 -0
  5. package/drizzle/0003_workspace_config_singleton.sql +16 -0
  6. package/drizzle/0004_terminal_claim_v120.sql +11 -0
  7. package/drizzle/0005_brainstorm_v124.sql +10 -0
  8. package/drizzle/0006_project_v140.sql +6 -0
  9. package/drizzle/0007_prd_wizard_v124.sql +14 -0
  10. package/drizzle/0008_workflow_artifact_v126.sql +24 -0
  11. package/drizzle/0009_project_import_v150.sql +7 -0
  12. package/drizzle/0010_quality_v151.sql +3 -0
  13. package/drizzle/meta/0000_snapshot.json +1335 -0
  14. package/drizzle/meta/0001_snapshot.json +1416 -0
  15. package/drizzle/meta/0002_snapshot.json +1510 -0
  16. package/drizzle/meta/_journal.json +83 -0
  17. package/package.json +41 -0
  18. package/plugin/.claude-plugin/marketplace.json +36 -0
  19. package/plugin/.claude-plugin/plugin.json +22 -0
  20. package/plugin/README.md +625 -0
  21. package/plugin/SKILLS.md +350 -0
  22. package/plugin/co2.svg +5 -0
  23. package/plugin/commands/brainstorm-loop.md +129 -0
  24. package/plugin/skills/conductor-defect/SKILL.md +898 -0
  25. package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
  26. package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
  27. package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
  28. package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
  29. package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
  30. package/plugin/skills/depgen-k8s/SKILL.md +468 -0
  31. package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
  32. package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
  33. package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
  34. package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
  35. package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
  36. package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
  37. package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
  38. package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
  39. package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
  40. package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
  41. package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
  42. package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
  43. package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
  44. package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
  45. package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
  46. package/plugin/skills/modelgen-relational/SKILL.md +707 -0
  47. package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
  48. package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
  49. package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
  50. package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
  51. package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
  52. package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
  53. package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
  54. package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
  55. package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
  56. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
  57. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
  58. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
  59. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
  60. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
  61. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
  62. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
  63. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
  64. package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
  65. package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
  66. package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
  67. package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
  68. package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
  69. package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
  70. package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
  71. package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
  72. package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
  73. package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
  74. package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
  75. package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
  76. package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
  77. package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
  78. package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
  79. package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
  80. package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
  81. package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
  82. package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
  83. package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
  84. package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
  85. package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
  86. package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
  87. package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
  88. package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
  89. package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
  90. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
  91. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
  92. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
  93. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
  94. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
  95. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
  96. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
  97. package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
  98. package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
  99. package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
  100. package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
  101. package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
  102. package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
  103. package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
  104. package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
  105. package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
  106. package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
  107. package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
  108. package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
  109. package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
  110. package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
  111. package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
  112. package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
  113. package/plugin/skills/testgen-functional/SKILL.md +1381 -0
  114. package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
  115. package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
  116. package/plugin/skills/util-projectinit/SKILL.md +307 -0
  117. package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
  118. package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
  119. package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
  120. package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
  121. package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
  122. package/plugin/skills/util-projectsync/SKILL.md +501 -0
  123. package/plugin/skills/util-updprd/SKILL.md +324 -0
  124. package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
  125. package/plugin/skills/util-ustagger/SKILL.md +154 -0
  126. package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
  127. package/static/assets/arc-DRKw7sjs.js +1 -0
  128. package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
  129. package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
  130. package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
  131. package/static/assets/channel-C2nVbuwv.js +1 -0
  132. package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
  133. package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
  134. package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
  135. package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
  136. package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
  137. package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
  138. package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
  139. package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
  140. package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
  141. package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
  142. package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
  143. package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
  144. package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
  145. package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
  146. package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
  147. package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
  148. package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
  149. package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  150. package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
  151. package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
  152. package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
  153. package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
  154. package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
  155. package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
  156. package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
  157. package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
  158. package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
  159. package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
  160. package/static/assets/graph-C9eacEi8.js +1 -0
  161. package/static/assets/index-LZSQkLE2.css +41 -0
  162. package/static/assets/index-y7XDSgMG.js +495 -0
  163. package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
  164. package/static/assets/init-Gi6I4Gst.js +1 -0
  165. package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
  166. package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
  167. package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
  168. package/static/assets/katex-C5jXJg4s.js +257 -0
  169. package/static/assets/layout-DEXfKzaS.js +1 -0
  170. package/static/assets/linear-CLO--ryk.js +1 -0
  171. package/static/assets/map-Czzmt4hB.js +1 -0
  172. package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
  173. package/static/assets/ordinal-Cboi1Yqb.js +1 -0
  174. package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
  175. package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
  176. package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
  177. package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
  178. package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
  179. package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
  180. package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
  181. package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
  182. package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
  183. package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
  184. package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
  185. package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
  186. package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
  187. package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
  188. package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
  189. package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
  190. package/static/index.html +24 -0
@@ -0,0 +1,1077 @@
1
+ # Component Patterns — MUI Theme, TanStack Query, Zustand, React Hook Form
2
+
3
+ This reference describes the reusable component and state patterns for the spec.
4
+ Include this content in Sections 5, 9, 10, 12, and 14 of the generated specification.
5
+
6
+ ---
7
+
8
+ ## MUI Theme Configuration
9
+
10
+ ### Theme Setup
11
+
12
+ ```typescript
13
+ // src/theme/palette.ts
14
+ import type { PaletteOptions } from '@mui/material/styles';
15
+
16
+ /**
17
+ * Design tokens extracted from the application mockup.
18
+ * Replace hex values with those found in MOCKUP.html CSS / inline styles.
19
+ */
20
+ export const palette: PaletteOptions = {
21
+ primary: {
22
+ main: '{{PRIMARY_COLOR}}', // e.g., '#2271b1' from mockup
23
+ dark: '{{PRIMARY_DARK_COLOR}}', // e.g., '#135e96' from mockup (hover state)
24
+ contrastText: '#ffffff',
25
+ },
26
+ secondary: {
27
+ main: '{{SECONDARY_COLOR}}',
28
+ contrastText: '#ffffff',
29
+ },
30
+ error: {
31
+ main: '{{ERROR_COLOR}}', // e.g., '#d63638' from mockup
32
+ },
33
+ warning: {
34
+ main: '{{WARNING_COLOR}}', // e.g., '#dba617' from mockup
35
+ },
36
+ success: {
37
+ main: '{{SUCCESS_COLOR}}', // e.g., '#00a32a' from mockup
38
+ },
39
+ background: {
40
+ default: '{{PAGE_BACKGROUND}}', // e.g., '#f0f0f1' from mockup
41
+ paper: '{{SURFACE_BACKGROUND}}', // e.g., '#ffffff' from mockup
42
+ },
43
+ text: {
44
+ primary: '{{TEXT_PRIMARY}}', // e.g., '#1e1e1e' from mockup
45
+ secondary: '{{TEXT_SECONDARY}}', // e.g., '#646970' from mockup
46
+ },
47
+ divider: '{{BORDER_COLOR}}', // e.g., '#c3c4c7' from mockup
48
+ };
49
+ ```
50
+
51
+ ```typescript
52
+ // src/theme/typography.ts
53
+ import type { TypographyOptions } from '@mui/material/styles/createTypography';
54
+
55
+ /**
56
+ * Typography tokens extracted from the application mockup.
57
+ */
58
+ export const typography: TypographyOptions = {
59
+ // Extract font family from mockup CSS font-family declaration
60
+ fontFamily: '{{FONT_FAMILY}}', // e.g., '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, ...'
61
+ h1: { fontSize: '2rem', fontWeight: 700 },
62
+ h2: { fontSize: '1.75rem', fontWeight: 700 },
63
+ h3: { fontSize: '1.5rem', fontWeight: 600 },
64
+ h4: { fontSize: '1.25rem', fontWeight: 600 },
65
+ h5: { fontSize: '1.125rem', fontWeight: 600 },
66
+ h6: { fontSize: '1rem', fontWeight: 600 },
67
+ body1: { fontSize: '1rem' },
68
+ body2: { fontSize: '0.875rem' },
69
+ caption: { fontSize: '0.75rem', color: '{{TEXT_SECONDARY}}' },
70
+ };
71
+ ```
72
+
73
+ ```typescript
74
+ // src/theme/index.ts
75
+ import { createTheme } from '@mui/material/styles';
76
+ import { palette } from './palette';
77
+ import { typography } from './typography';
78
+
79
+ export const lightTheme = createTheme({
80
+ palette: { mode: 'light', ...palette },
81
+ typography,
82
+ shape: {
83
+ // Extract border-radius from mockup
84
+ borderRadius: {{BORDER_RADIUS_BASE}}, // e.g., 4
85
+ },
86
+ components: {
87
+ MuiButton: {
88
+ styleOverrides: {
89
+ root: {
90
+ borderRadius: {{BORDER_RADIUS_BUTTON}}, // e.g., 4 for buttons
91
+ textTransform: 'none',
92
+ fontWeight: 600,
93
+ },
94
+ },
95
+ defaultProps: {
96
+ disableElevation: true,
97
+ },
98
+ },
99
+ MuiCard: {
100
+ styleOverrides: {
101
+ root: {
102
+ borderRadius: {{BORDER_RADIUS_CARD}}, // e.g., 8 for cards
103
+ boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
104
+ },
105
+ },
106
+ },
107
+ MuiTextField: {
108
+ defaultProps: {
109
+ size: 'small',
110
+ variant: 'outlined',
111
+ },
112
+ },
113
+ MuiPaper: {
114
+ styleOverrides: {
115
+ root: {
116
+ borderRadius: {{BORDER_RADIUS_CARD}},
117
+ },
118
+ },
119
+ },
120
+ MuiTableCell: {
121
+ styleOverrides: {
122
+ head: {
123
+ fontWeight: 600,
124
+ backgroundColor: '{{PAGE_BACKGROUND}}',
125
+ },
126
+ },
127
+ },
128
+ },
129
+ });
130
+
131
+ export const darkTheme = createTheme({
132
+ palette: { mode: 'dark' },
133
+ typography,
134
+ shape: { borderRadius: {{BORDER_RADIUS_BASE}} },
135
+ });
136
+ ```
137
+
138
+ ---
139
+
140
+ ## TanStack Query Setup
141
+
142
+ ```typescript
143
+ // src/lib/queryClient.ts
144
+ import { QueryClient } from '@tanstack/react-query';
145
+ import { useNotificationStore } from '../store/notification.store';
146
+
147
+ export const queryClient = new QueryClient({
148
+ defaultOptions: {
149
+ queries: {
150
+ // Data is considered fresh for 30 seconds — prevents redundant refetches
151
+ staleTime: 30_000,
152
+ // Unused data is garbage collected after 5 minutes
153
+ gcTime: 5 * 60_000,
154
+ // Retry failed queries up to 2 times (not for 4xx errors)
155
+ retry: (failureCount, error) => {
156
+ if (error instanceof Error && 'status' in error) {
157
+ const status = (error as { status: number }).status;
158
+ if (status >= 400 && status < 500) return false;
159
+ }
160
+ return failureCount < 2;
161
+ },
162
+ refetchOnWindowFocus: true,
163
+ },
164
+ mutations: {
165
+ onError: (error) => {
166
+ const message =
167
+ error instanceof Error ? error.message : 'An unexpected error occurred';
168
+ useNotificationStore.getState().push('error', message);
169
+ },
170
+ },
171
+ },
172
+ });
173
+ ```
174
+
175
+ ### Standard Query Hook Pattern
176
+
177
+ ```typescript
178
+ // Example: src/features/heroSection/hooks/useHeroSections.ts
179
+ import { useQuery } from '@tanstack/react-query';
180
+ import { heroSectionApi } from '../api/heroSectionApi';
181
+ import type { HeroSectionListParams } from '../heroSection.types';
182
+
183
+ export const heroSectionKeys = {
184
+ all: ['heroSections'] as const,
185
+ lists: () => [...heroSectionKeys.all, 'list'] as const,
186
+ list: (params: HeroSectionListParams) => [...heroSectionKeys.lists(), params] as const,
187
+ details: () => [...heroSectionKeys.all, 'detail'] as const,
188
+ detail: (id: string) => [...heroSectionKeys.details(), id] as const,
189
+ };
190
+
191
+ export function useHeroSections(params: HeroSectionListParams) {
192
+ return useQuery({
193
+ queryKey: heroSectionKeys.list(params),
194
+ queryFn: () => heroSectionApi.list(params),
195
+ });
196
+ }
197
+
198
+ export function useHeroSection(id: string) {
199
+ return useQuery({
200
+ queryKey: heroSectionKeys.detail(id),
201
+ queryFn: () => heroSectionApi.getById(id),
202
+ enabled: !!id,
203
+ });
204
+ }
205
+ ```
206
+
207
+ ### Standard Mutation Hook Pattern
208
+
209
+ ```typescript
210
+ // Example: src/features/heroSection/hooks/useHeroSectionMutations.ts
211
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
212
+ import { heroSectionApi } from '../api/heroSectionApi';
213
+ import { heroSectionKeys } from './useHeroSections';
214
+ import { useNotificationStore } from '../../../store/notification.store';
215
+ import type { HeroSectionCreate, HeroSectionUpdate } from '../heroSection.types';
216
+
217
+ export function useCreateHeroSection() {
218
+ const queryClient = useQueryClient();
219
+ const { push } = useNotificationStore();
220
+
221
+ return useMutation({
222
+ mutationFn: (data: HeroSectionCreate) => heroSectionApi.create(data),
223
+ onSuccess: () => {
224
+ // Invalidate the list query so it refetches with the new item
225
+ queryClient.invalidateQueries({ queryKey: heroSectionKeys.lists() });
226
+ push('success', 'Hero section created successfully.');
227
+ },
228
+ });
229
+ }
230
+
231
+ export function useUpdateHeroSection() {
232
+ const queryClient = useQueryClient();
233
+ const { push } = useNotificationStore();
234
+
235
+ return useMutation({
236
+ mutationFn: ({ id, data }: { id: string; data: HeroSectionUpdate }) =>
237
+ heroSectionApi.update(id, data),
238
+ onSuccess: (_, { id }) => {
239
+ queryClient.invalidateQueries({ queryKey: heroSectionKeys.detail(id) });
240
+ queryClient.invalidateQueries({ queryKey: heroSectionKeys.lists() });
241
+ push('success', 'Hero section updated successfully.');
242
+ },
243
+ });
244
+ }
245
+
246
+ export function useDeleteHeroSection() {
247
+ const queryClient = useQueryClient();
248
+ const { push } = useNotificationStore();
249
+
250
+ return useMutation({
251
+ mutationFn: (id: string) => heroSectionApi.delete(id),
252
+ onSuccess: () => {
253
+ queryClient.invalidateQueries({ queryKey: heroSectionKeys.lists() });
254
+ push('success', 'Hero section deleted.');
255
+ },
256
+ });
257
+ }
258
+ ```
259
+
260
+ ---
261
+
262
+ ## Zustand Stores
263
+
264
+ ### Notification Store
265
+
266
+ ```typescript
267
+ // src/store/notification.store.ts
268
+ import { create } from 'zustand';
269
+
270
+ type NotificationType = 'success' | 'error' | 'warning' | 'info';
271
+
272
+ export interface Notification {
273
+ id: string;
274
+ type: NotificationType;
275
+ message: string;
276
+ }
277
+
278
+ interface NotificationState {
279
+ notifications: Notification[];
280
+ push: (type: NotificationType, message: string) => void;
281
+ dismiss: (id: string) => void;
282
+ }
283
+
284
+ export const useNotificationStore = create<NotificationState>((set) => ({
285
+ notifications: [],
286
+
287
+ push: (type, message) => {
288
+ const id = crypto.randomUUID();
289
+ set((state) => ({ notifications: [...state.notifications, { id, type, message }] }));
290
+ // Auto-dismiss after 5 seconds
291
+ setTimeout(() => {
292
+ set((state) => ({
293
+ notifications: state.notifications.filter((n) => n.id !== id),
294
+ }));
295
+ }, 5_000);
296
+ },
297
+
298
+ dismiss: (id) =>
299
+ set((state) => ({ notifications: state.notifications.filter((n) => n.id !== id) })),
300
+ }));
301
+ ```
302
+
303
+ ### UI Store
304
+
305
+ ```typescript
306
+ // src/store/ui.store.ts
307
+ import { create } from 'zustand';
308
+ import { persist } from 'zustand/middleware';
309
+
310
+ interface UiState {
311
+ sidebarOpen: boolean;
312
+ themeMode: 'light' | 'dark';
313
+ toggleSidebar: () => void;
314
+ toggleThemeMode: () => void;
315
+ }
316
+
317
+ export const useUiStore = create<UiState>()(
318
+ persist(
319
+ (set) => ({
320
+ sidebarOpen: true,
321
+ themeMode: 'light',
322
+ toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
323
+ toggleThemeMode: () =>
324
+ set((state) => ({ themeMode: state.themeMode === 'light' ? 'dark' : 'light' })),
325
+ }),
326
+ { name: 'ui-preferences' }, // Persisted to localStorage (safe — no tokens here)
327
+ ),
328
+ );
329
+ ```
330
+
331
+ ---
332
+
333
+ ## React Hook Form + Zod Patterns
334
+
335
+ ### Zod Schema Example
336
+
337
+ ```typescript
338
+ // Example: src/features/heroSection/heroSection.schema.ts
339
+ import { z } from 'zod';
340
+
341
+ /**
342
+ * Validation schema for hero section create/update form.
343
+ * Rules derived from PRD.md NFRs and constraints.
344
+ */
345
+ export const heroSectionSchema = z
346
+ .object({
347
+ headline: z
348
+ .string()
349
+ .min(1, 'Headline is required')
350
+ .max(100, 'Headline must be at most 100 characters'),
351
+ subheadline: z
352
+ .string()
353
+ .min(1, 'Subheadline is required')
354
+ .max(200, 'Subheadline must be at most 200 characters'),
355
+ ctaText: z
356
+ .string()
357
+ .min(1, 'CTA text is required')
358
+ .max(50, 'CTA text must be at most 50 characters'),
359
+ ctaUrl: z
360
+ .string()
361
+ .url('Must be a valid URL')
362
+ .min(1, 'CTA URL is required'),
363
+ effectiveDate: z.string().min(1, 'Effective date is required'),
364
+ expirationDate: z.string().min(1, 'Expiration date is required'),
365
+ // image is optional in schema — uploaded separately via multipart
366
+ })
367
+ .refine(
368
+ (data) => new Date(data.expirationDate) > new Date(data.effectiveDate),
369
+ {
370
+ message: 'Expiration date must be after effective date',
371
+ path: ['expirationDate'],
372
+ },
373
+ );
374
+
375
+ export type HeroSectionFormValues = z.infer<typeof heroSectionSchema>;
376
+ ```
377
+
378
+ ### Form Component Pattern
379
+
380
+ ```tsx
381
+ // Example: src/features/heroSection/components/HeroSectionForm.tsx
382
+ import { useForm } from 'react-hook-form';
383
+ import { zodResolver } from '@hookform/resolvers/zod';
384
+ import { Button, Stack, Box } from '@mui/material';
385
+ import { TextFieldController } from '../../../shared/form/TextFieldController';
386
+ import { DatePickerController } from '../../../shared/form/DatePickerController';
387
+ import { ImageUpload } from '../../../shared/components/ImageUpload';
388
+ import { heroSectionSchema, type HeroSectionFormValues } from '../heroSection.schema';
389
+ import type { HeroSection } from '../heroSection.types';
390
+
391
+ interface HeroSectionFormProps {
392
+ defaultValues?: Partial<HeroSectionFormValues>;
393
+ onSubmit: (values: HeroSectionFormValues, imageFile?: File) => void;
394
+ isSubmitting: boolean;
395
+ /** If provided, "Cancel" button navigates back */
396
+ onCancel: () => void;
397
+ }
398
+
399
+ export function HeroSectionForm({
400
+ defaultValues,
401
+ onSubmit,
402
+ isSubmitting,
403
+ onCancel,
404
+ }: HeroSectionFormProps) {
405
+ const { control, handleSubmit } = useForm<HeroSectionFormValues>({
406
+ resolver: zodResolver(heroSectionSchema),
407
+ defaultValues: {
408
+ headline: '',
409
+ subheadline: '',
410
+ ctaText: '',
411
+ ctaUrl: '',
412
+ effectiveDate: '',
413
+ expirationDate: '',
414
+ ...defaultValues,
415
+ },
416
+ });
417
+
418
+ const [imageFile, setImageFile] = useState<File | undefined>();
419
+
420
+ const handleFormSubmit = (values: HeroSectionFormValues) => {
421
+ onSubmit(values, imageFile);
422
+ };
423
+
424
+ return (
425
+ <Box component="form" onSubmit={handleSubmit(handleFormSubmit)} noValidate>
426
+ <Stack spacing={3}>
427
+ <ImageUpload
428
+ label="Hero Image (1600x500 px)"
429
+ onFileChange={setImageFile}
430
+ aspectRatio="1600/500"
431
+ helperText="Image must be exactly 1600×500 pixels"
432
+ />
433
+ <TextFieldController
434
+ name="headline"
435
+ control={control}
436
+ label="Headline"
437
+ required
438
+ inputProps={{ maxLength: 100 }}
439
+ helperText="Max 100 characters"
440
+ />
441
+ <TextFieldController
442
+ name="subheadline"
443
+ control={control}
444
+ label="Subheadline"
445
+ required
446
+ multiline
447
+ rows={2}
448
+ inputProps={{ maxLength: 200 }}
449
+ />
450
+ <TextFieldController
451
+ name="ctaText"
452
+ control={control}
453
+ label="CTA Button Text"
454
+ required
455
+ inputProps={{ maxLength: 50 }}
456
+ />
457
+ <TextFieldController
458
+ name="ctaUrl"
459
+ control={control}
460
+ label="CTA Button URL"
461
+ required
462
+ placeholder="https://..."
463
+ />
464
+ <Stack direction="row" spacing={2}>
465
+ <DatePickerController
466
+ name="effectiveDate"
467
+ control={control}
468
+ label="Effective Date"
469
+ required
470
+ />
471
+ <DatePickerController
472
+ name="expirationDate"
473
+ control={control}
474
+ label="Expiration Date"
475
+ required
476
+ />
477
+ </Stack>
478
+
479
+ <Stack direction="row" spacing={2} justifyContent="flex-end">
480
+ <Button variant="outlined" onClick={onCancel}>
481
+ Cancel
482
+ </Button>
483
+ <Button type="submit" variant="contained" loading={isSubmitting}>
484
+ {defaultValues ? 'Update' : 'Create'}
485
+ </Button>
486
+ </Stack>
487
+ </Stack>
488
+ </Box>
489
+ );
490
+ }
491
+ ```
492
+
493
+ ### RHF Controller Wrappers
494
+
495
+ ```tsx
496
+ // src/shared/form/TextFieldController.tsx
497
+ import { Controller, type Control, type FieldPath, type FieldValues } from 'react-hook-form';
498
+ import { TextField, type TextFieldProps } from '@mui/material';
499
+
500
+ type TextFieldControllerProps<T extends FieldValues> = Omit<
501
+ TextFieldProps,
502
+ 'name' | 'value' | 'onChange' | 'error' | 'helperText'
503
+ > & {
504
+ name: FieldPath<T>;
505
+ control: Control<T>;
506
+ };
507
+
508
+ export function TextFieldController<T extends FieldValues>({
509
+ name,
510
+ control,
511
+ ...textFieldProps
512
+ }: TextFieldControllerProps<T>) {
513
+ return (
514
+ <Controller
515
+ name={name}
516
+ control={control}
517
+ render={({ field, fieldState }) => (
518
+ <TextField
519
+ {...field}
520
+ {...textFieldProps}
521
+ error={!!fieldState.error}
522
+ helperText={fieldState.error?.message ?? textFieldProps.helperText}
523
+ fullWidth
524
+ />
525
+ )}
526
+ />
527
+ );
528
+ }
529
+ ```
530
+
531
+ ```tsx
532
+ // src/shared/form/SelectController.tsx
533
+ import { Controller, type Control, type FieldPath, type FieldValues } from 'react-hook-form';
534
+ import {
535
+ FormControl,
536
+ FormHelperText,
537
+ InputLabel,
538
+ MenuItem,
539
+ Select,
540
+ type SelectProps,
541
+ } from '@mui/material';
542
+
543
+ export interface SelectOption {
544
+ value: string | number;
545
+ label: string;
546
+ }
547
+
548
+ type SelectControllerProps<T extends FieldValues> = Omit<
549
+ SelectProps,
550
+ 'name' | 'value' | 'onChange' | 'error'
551
+ > & {
552
+ name: FieldPath<T>;
553
+ control: Control<T>;
554
+ label: string;
555
+ options: SelectOption[];
556
+ helperText?: string;
557
+ };
558
+
559
+ export function SelectController<T extends FieldValues>({
560
+ name,
561
+ control,
562
+ label,
563
+ options,
564
+ helperText,
565
+ ...selectProps
566
+ }: SelectControllerProps<T>) {
567
+ return (
568
+ <Controller
569
+ name={name}
570
+ control={control}
571
+ render={({ field, fieldState }) => (
572
+ <FormControl fullWidth error={!!fieldState.error} size="small">
573
+ <InputLabel>{label}</InputLabel>
574
+ <Select {...field} {...selectProps} label={label}>
575
+ {options.map((opt) => (
576
+ <MenuItem key={opt.value} value={opt.value}>
577
+ {opt.label}
578
+ </MenuItem>
579
+ ))}
580
+ </Select>
581
+ <FormHelperText>
582
+ {fieldState.error?.message ?? helperText}
583
+ </FormHelperText>
584
+ </FormControl>
585
+ )}
586
+ />
587
+ );
588
+ }
589
+ ```
590
+
591
+ ---
592
+
593
+ ## Notification Provider
594
+
595
+ ```tsx
596
+ // src/shared/components/NotificationProvider.tsx
597
+ import { Alert, Snackbar, Stack } from '@mui/material';
598
+ import { useNotificationStore } from '../../store/notification.store';
599
+
600
+ export function NotificationProvider() {
601
+ const { notifications, dismiss } = useNotificationStore();
602
+
603
+ return (
604
+ <Stack
605
+ spacing={1}
606
+ sx={{ position: 'fixed', bottom: 24, right: 24, zIndex: 9999, maxWidth: 400 }}
607
+ >
608
+ {notifications.map((n) => (
609
+ <Snackbar
610
+ key={n.id}
611
+ open
612
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
613
+ >
614
+ <Alert
615
+ severity={n.type}
616
+ onClose={() => dismiss(n.id)}
617
+ variant="filled"
618
+ elevation={6}
619
+ >
620
+ {n.message}
621
+ </Alert>
622
+ </Snackbar>
623
+ ))}
624
+ </Stack>
625
+ );
626
+ }
627
+ ```
628
+
629
+ ---
630
+
631
+ ## Status Chip Component
632
+
633
+ ```tsx
634
+ // src/shared/components/StatusChip.tsx
635
+ import { Chip, type ChipProps } from '@mui/material';
636
+
637
+ type StatusColor = 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning';
638
+
639
+ interface StatusConfig {
640
+ label: string;
641
+ color: StatusColor;
642
+ }
643
+
644
+ interface StatusChipProps extends Omit<ChipProps, 'color' | 'label'> {
645
+ status: string;
646
+ /** Map of status value → display config. Provide this per-module. */
647
+ statusMap: Record<string, StatusConfig>;
648
+ }
649
+
650
+ export function StatusChip({ status, statusMap, ...chipProps }: StatusChipProps) {
651
+ const config = statusMap[status] ?? { label: status, color: 'default' as StatusColor };
652
+
653
+ return (
654
+ <Chip
655
+ label={config.label}
656
+ color={config.color}
657
+ size="small"
658
+ {...chipProps}
659
+ />
660
+ );
661
+ }
662
+
663
+ // Example usage in a module:
664
+ // const HERO_STATUS_MAP = {
665
+ // DRAFT: { label: 'Draft', color: 'default' },
666
+ // ACTIVE: { label: 'Active', color: 'success' },
667
+ // EXPIRED: { label: 'Expired', color: 'error' },
668
+ // };
669
+ // <StatusChip status={row.status} statusMap={HERO_STATUS_MAP} />
670
+ ```
671
+
672
+ ---
673
+
674
+ ## Confirm Dialog Hook
675
+
676
+ ```tsx
677
+ // src/shared/components/ConfirmDialog.tsx
678
+ import {
679
+ Button,
680
+ Dialog,
681
+ DialogActions,
682
+ DialogContent,
683
+ DialogContentText,
684
+ DialogTitle,
685
+ } from '@mui/material';
686
+ import { create } from 'zustand';
687
+
688
+ interface ConfirmState {
689
+ open: boolean;
690
+ title: string;
691
+ message: string;
692
+ confirmLabel: string;
693
+ cancelLabel: string;
694
+ onConfirm: (() => void) | null;
695
+ show: (options: Omit<ConfirmState, 'open' | 'show' | 'hide'>) => void;
696
+ hide: () => void;
697
+ }
698
+
699
+ const useConfirmStore = create<ConfirmState>((set) => ({
700
+ open: false,
701
+ title: '',
702
+ message: '',
703
+ confirmLabel: 'Confirm',
704
+ cancelLabel: 'Cancel',
705
+ onConfirm: null,
706
+ show: (options) => set({ open: true, ...options }),
707
+ hide: () => set({ open: false, onConfirm: null }),
708
+ }));
709
+
710
+ /** Hook to imperatively trigger a confirmation dialog */
711
+ export function useConfirm() {
712
+ const { show } = useConfirmStore();
713
+
714
+ return (options: {
715
+ title?: string;
716
+ message: string;
717
+ confirmLabel?: string;
718
+ cancelLabel?: string;
719
+ }): Promise<boolean> =>
720
+ new Promise((resolve) => {
721
+ show({
722
+ title: options.title ?? 'Confirm Action',
723
+ message: options.message,
724
+ confirmLabel: options.confirmLabel ?? 'Confirm',
725
+ cancelLabel: options.cancelLabel ?? 'Cancel',
726
+ onConfirm: () => resolve(true),
727
+ });
728
+ });
729
+ }
730
+
731
+ /** Render this once at the app root to display the dialog */
732
+ export function ConfirmDialog() {
733
+ const { open, title, message, confirmLabel, cancelLabel, onConfirm, hide } =
734
+ useConfirmStore();
735
+
736
+ const handleConfirm = () => {
737
+ onConfirm?.();
738
+ hide();
739
+ };
740
+
741
+ return (
742
+ <Dialog open={open} onClose={hide} maxWidth="xs" fullWidth>
743
+ <DialogTitle>{title}</DialogTitle>
744
+ <DialogContent>
745
+ <DialogContentText>{message}</DialogContentText>
746
+ </DialogContent>
747
+ <DialogActions>
748
+ <Button onClick={hide} color="inherit">
749
+ {cancelLabel}
750
+ </Button>
751
+ <Button onClick={handleConfirm} color="error" variant="contained" autoFocus>
752
+ {confirmLabel}
753
+ </Button>
754
+ </DialogActions>
755
+ </Dialog>
756
+ );
757
+ }
758
+ ```
759
+
760
+ ---
761
+
762
+ ## API Function Pattern
763
+
764
+ ```typescript
765
+ // Example: src/features/heroSection/api/heroSectionApi.ts
766
+ import { axiosInstance } from '../../../lib/api/axiosInstance';
767
+ import type {
768
+ HeroSection,
769
+ HeroSectionCreate,
770
+ HeroSectionUpdate,
771
+ HeroSectionListParams,
772
+ PaginatedResponse,
773
+ } from '../heroSection.types';
774
+
775
+ export const heroSectionApi = {
776
+ /**
777
+ * Fetch paginated list of hero sections.
778
+ */
779
+ list: async (params: HeroSectionListParams): Promise<PaginatedResponse<HeroSection>> => {
780
+ const { data } = await axiosInstance.get<PaginatedResponse<HeroSection>>(
781
+ '/hero-sections',
782
+ { params },
783
+ );
784
+ return data;
785
+ },
786
+
787
+ /**
788
+ * Fetch a single hero section by ID.
789
+ */
790
+ getById: async (id: string): Promise<HeroSection> => {
791
+ const { data } = await axiosInstance.get<HeroSection>(`/hero-sections/${id}`);
792
+ return data;
793
+ },
794
+
795
+ /**
796
+ * Create a new hero section with an optional image upload.
797
+ * Uses multipart/form-data when an image file is provided.
798
+ */
799
+ create: async (payload: HeroSectionCreate, imageFile?: File): Promise<HeroSection> => {
800
+ if (imageFile) {
801
+ const formData = new FormData();
802
+ formData.append('data', JSON.stringify(payload));
803
+ formData.append('image', imageFile);
804
+ const { data } = await axiosInstance.post<HeroSection>('/hero-sections', formData, {
805
+ headers: { 'Content-Type': 'multipart/form-data' },
806
+ });
807
+ return data;
808
+ }
809
+ const { data } = await axiosInstance.post<HeroSection>('/hero-sections', payload);
810
+ return data;
811
+ },
812
+
813
+ /**
814
+ * Update an existing hero section.
815
+ */
816
+ update: async (
817
+ id: string,
818
+ payload: HeroSectionUpdate,
819
+ imageFile?: File,
820
+ ): Promise<HeroSection> => {
821
+ if (imageFile) {
822
+ const formData = new FormData();
823
+ formData.append('data', JSON.stringify(payload));
824
+ formData.append('image', imageFile);
825
+ const { data } = await axiosInstance.put<HeroSection>(
826
+ `/hero-sections/${id}`,
827
+ formData,
828
+ { headers: { 'Content-Type': 'multipart/form-data' } },
829
+ );
830
+ return data;
831
+ }
832
+ const { data } = await axiosInstance.put<HeroSection>(`/hero-sections/${id}`, payload);
833
+ return data;
834
+ },
835
+
836
+ /**
837
+ * Delete a hero section by ID.
838
+ */
839
+ delete: async (id: string): Promise<void> => {
840
+ await axiosInstance.delete(`/hero-sections/${id}`);
841
+ },
842
+
843
+ /**
844
+ * Serve image by ID (returns a URL for use in <img src="..."> via Blob URL or direct endpoint).
845
+ */
846
+ imageUrl: (id: string): string =>
847
+ `${import.meta.env.VITE_API_BASE_URL}/hero-sections/${id}/image`,
848
+ };
849
+ ```
850
+
851
+ ---
852
+
853
+ ## TypeScript Types Pattern
854
+
855
+ ```typescript
856
+ // Example: src/features/heroSection/heroSection.types.ts
857
+
858
+ /** Enum derived from PRD.md constraint for hero section status */
859
+ export type HeroSectionStatus = 'DRAFT' | 'ACTIVE' | 'EXPIRED';
860
+
861
+ /** Full hero section entity returned by the API */
862
+ export interface HeroSection {
863
+ id: string;
864
+ headline: string;
865
+ subheadline: string;
866
+ ctaText: string;
867
+ ctaUrl: string;
868
+ effectiveDate: string; // ISO date string (YYYY-MM-DD)
869
+ expirationDate: string; // ISO date string
870
+ status: HeroSectionStatus;
871
+ createdAt: string; // ISO datetime string
872
+ updatedAt: string;
873
+ }
874
+
875
+ /** Payload for creating a hero section (image uploaded separately) */
876
+ export interface HeroSectionCreate {
877
+ headline: string;
878
+ subheadline: string;
879
+ ctaText: string;
880
+ ctaUrl: string;
881
+ effectiveDate: string;
882
+ expirationDate: string;
883
+ }
884
+
885
+ /** Payload for updating a hero section */
886
+ export type HeroSectionUpdate = Partial<HeroSectionCreate>;
887
+
888
+ /** Query parameters for the list endpoint */
889
+ export interface HeroSectionListParams {
890
+ page?: number;
891
+ size?: number;
892
+ status?: HeroSectionStatus | '';
893
+ effectiveDateFrom?: string;
894
+ effectiveDateTo?: string;
895
+ }
896
+
897
+ /** Generic paginated response wrapper — matches the backend API contract */
898
+ export interface PaginatedResponse<T> {
899
+ content: T[];
900
+ page: number;
901
+ size: number;
902
+ totalElements: number;
903
+ totalPages: number;
904
+ }
905
+ ```
906
+
907
+ ---
908
+
909
+ ## Page Component Pattern
910
+
911
+ ```tsx
912
+ // Example: src/features/heroSection/pages/HeroSectionListPage.tsx
913
+ import { useState } from 'react';
914
+ import { useNavigate } from 'react-router-dom';
915
+ import {
916
+ Box,
917
+ Button,
918
+ Card,
919
+ Chip,
920
+ IconButton,
921
+ MenuItem,
922
+ Select,
923
+ Stack,
924
+ Table,
925
+ TableBody,
926
+ TableCell,
927
+ TableContainer,
928
+ TableHead,
929
+ TablePagination,
930
+ TableRow,
931
+ Tooltip,
932
+ } from '@mui/material';
933
+ import AddIcon from '@mui/icons-material/Add';
934
+ import EditIcon from '@mui/icons-material/Edit';
935
+ import DeleteIcon from '@mui/icons-material/Delete';
936
+ import { PageHeader } from '../../../shared/components/PageHeader';
937
+ import { StatusChip } from '../../../shared/components/StatusChip';
938
+ import { LoadingOverlay } from '../../../shared/components/LoadingOverlay';
939
+ import { EmptyState } from '../../../shared/components/EmptyState';
940
+ import { useConfirm } from '../../../shared/components/ConfirmDialog';
941
+ import { useHeroSections } from '../hooks/useHeroSections';
942
+ import { useDeleteHeroSection } from '../hooks/useHeroSectionMutations';
943
+ import type { HeroSectionListParams, HeroSectionStatus } from '../heroSection.types';
944
+
945
+ const HERO_STATUS_MAP = {
946
+ DRAFT: { label: 'Draft', color: 'default' as const },
947
+ ACTIVE: { label: 'Active', color: 'success' as const },
948
+ EXPIRED: { label: 'Expired', color: 'error' as const },
949
+ };
950
+
951
+ export function HeroSectionListPage() {
952
+ const navigate = useNavigate();
953
+ const confirm = useConfirm();
954
+
955
+ const [params, setParams] = useState<HeroSectionListParams>({
956
+ page: 0,
957
+ size: 10,
958
+ status: '',
959
+ });
960
+
961
+ const { data, isLoading, isError } = useHeroSections(params);
962
+ const deleteMutation = useDeleteHeroSection();
963
+
964
+ const handleDelete = async (id: string, headline: string) => {
965
+ const confirmed = await confirm({
966
+ title: 'Delete Hero Section',
967
+ message: `Are you sure you want to delete "${headline}"? This action cannot be undone.`,
968
+ confirmLabel: 'Delete',
969
+ });
970
+ if (confirmed) {
971
+ deleteMutation.mutate(id);
972
+ }
973
+ };
974
+
975
+ if (isLoading) return <LoadingOverlay />;
976
+ if (isError) return <div>Failed to load hero sections.</div>;
977
+
978
+ return (
979
+ <Box>
980
+ <PageHeader
981
+ title="Hero Sections"
982
+ breadcrumbs={[{ label: 'Dashboard', path: '/dashboard' }, { label: 'Hero Sections' }]}
983
+ actions={
984
+ <Button
985
+ variant="contained"
986
+ startIcon={<AddIcon />}
987
+ onClick={() => navigate('/hero-section/create')}
988
+ >
989
+ Add Hero Section
990
+ </Button>
991
+ }
992
+ />
993
+
994
+ {/* Filters */}
995
+ <Stack direction="row" spacing={2} sx={{ mb: 2 }}>
996
+ <Select
997
+ size="small"
998
+ value={params.status ?? ''}
999
+ onChange={(e) => setParams((p) => ({ ...p, status: e.target.value as HeroSectionStatus | '', page: 0 }))}
1000
+ displayEmpty
1001
+ sx={{ minWidth: 160 }}
1002
+ >
1003
+ <MenuItem value="">All Statuses</MenuItem>
1004
+ <MenuItem value="DRAFT">Draft</MenuItem>
1005
+ <MenuItem value="ACTIVE">Active</MenuItem>
1006
+ <MenuItem value="EXPIRED">Expired</MenuItem>
1007
+ </Select>
1008
+ </Stack>
1009
+
1010
+ <Card>
1011
+ <TableContainer>
1012
+ <Table>
1013
+ <TableHead>
1014
+ <TableRow>
1015
+ <TableCell>Headline</TableCell>
1016
+ <TableCell>Effective Date</TableCell>
1017
+ <TableCell>Expiration Date</TableCell>
1018
+ <TableCell>Status</TableCell>
1019
+ <TableCell align="right">Actions</TableCell>
1020
+ </TableRow>
1021
+ </TableHead>
1022
+ <TableBody>
1023
+ {data?.content.length === 0 ? (
1024
+ <TableRow>
1025
+ <TableCell colSpan={5}>
1026
+ <EmptyState message="No hero sections found." />
1027
+ </TableCell>
1028
+ </TableRow>
1029
+ ) : (
1030
+ data?.content.map((row) => (
1031
+ <TableRow key={row.id} hover>
1032
+ <TableCell>{row.headline}</TableCell>
1033
+ <TableCell>{row.effectiveDate}</TableCell>
1034
+ <TableCell>{row.expirationDate}</TableCell>
1035
+ <TableCell>
1036
+ <StatusChip status={row.status} statusMap={HERO_STATUS_MAP} />
1037
+ </TableCell>
1038
+ <TableCell align="right">
1039
+ <Tooltip title="Edit">
1040
+ <IconButton
1041
+ size="small"
1042
+ onClick={() => navigate(`/hero-section/${row.id}/edit`)}
1043
+ >
1044
+ <EditIcon fontSize="small" />
1045
+ </IconButton>
1046
+ </Tooltip>
1047
+ <Tooltip title="Delete">
1048
+ <IconButton
1049
+ size="small"
1050
+ color="error"
1051
+ onClick={() => handleDelete(row.id, row.headline)}
1052
+ >
1053
+ <DeleteIcon fontSize="small" />
1054
+ </IconButton>
1055
+ </Tooltip>
1056
+ </TableCell>
1057
+ </TableRow>
1058
+ ))
1059
+ )}
1060
+ </TableBody>
1061
+ </Table>
1062
+ </TableContainer>
1063
+ <TablePagination
1064
+ component="div"
1065
+ count={data?.totalElements ?? 0}
1066
+ page={params.page ?? 0}
1067
+ rowsPerPage={params.size ?? 10}
1068
+ onPageChange={(_, page) => setParams((p) => ({ ...p, page }))}
1069
+ onRowsPerPageChange={(e) =>
1070
+ setParams((p) => ({ ...p, size: parseInt(e.target.value), page: 0 }))
1071
+ }
1072
+ />
1073
+ </Card>
1074
+ </Box>
1075
+ );
1076
+ }
1077
+ ```