keynesol-shared 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.
Files changed (154) hide show
  1. package/README.md +118 -0
  2. package/dist/components/Common/ErrorBoundary.d.ts +23 -0
  3. package/dist/components/Common/ErrorBoundary.d.ts.map +1 -0
  4. package/dist/components/Common/ErrorBoundary.js +93 -0
  5. package/dist/components/Common/ErrorBoundary.jsx +103 -0
  6. package/dist/components/Common/ErrorMessage.d.ts +8 -0
  7. package/dist/components/Common/ErrorMessage.d.ts.map +1 -0
  8. package/dist/components/Common/ErrorMessage.js +36 -0
  9. package/dist/components/Common/ErrorMessage.jsx +40 -0
  10. package/dist/components/Common/Loading.d.ts +8 -0
  11. package/dist/components/Common/Loading.d.ts.map +1 -0
  12. package/dist/components/Common/Loading.js +41 -0
  13. package/dist/components/Common/Loading.jsx +44 -0
  14. package/dist/components/Common/LoadingIndicator.d.ts +17 -0
  15. package/dist/components/Common/LoadingIndicator.d.ts.map +1 -0
  16. package/dist/components/Common/LoadingIndicator.js +95 -0
  17. package/dist/components/Common/LoadingIndicator.jsx +108 -0
  18. package/dist/components/Common/ProgramStatus.d.ts +3 -0
  19. package/dist/components/Common/ProgramStatus.d.ts.map +1 -0
  20. package/dist/components/Common/ProgramStatus.js +26 -0
  21. package/dist/components/Common/ProgramStatus.jsx +27 -0
  22. package/dist/components/Common/Skeleton.d.ts +39 -0
  23. package/dist/components/Common/Skeleton.d.ts.map +1 -0
  24. package/dist/components/Common/Skeleton.js +53 -0
  25. package/dist/components/Common/Skeleton.jsx +67 -0
  26. package/dist/components/Common/SkeletonScreen.d.ts +18 -0
  27. package/dist/components/Common/SkeletonScreen.d.ts.map +1 -0
  28. package/dist/components/Common/SkeletonScreen.js +98 -0
  29. package/dist/components/Common/SkeletonScreen.jsx +108 -0
  30. package/dist/components/Common/index.d.ts +11 -0
  31. package/dist/components/Common/index.d.ts.map +1 -0
  32. package/dist/components/Common/index.js +10 -0
  33. package/dist/components/Wallet/TransactionStatus.d.ts +11 -0
  34. package/dist/components/Wallet/TransactionStatus.d.ts.map +1 -0
  35. package/dist/components/Wallet/TransactionStatus.js +97 -0
  36. package/dist/components/Wallet/TransactionStatus.jsx +106 -0
  37. package/dist/components/Wallet/WalletBalance.d.ts +4 -0
  38. package/dist/components/Wallet/WalletBalance.d.ts.map +1 -0
  39. package/dist/components/Wallet/WalletBalance.js +82 -0
  40. package/dist/components/Wallet/WalletBalance.jsx +86 -0
  41. package/dist/components/Wallet/WalletButton.d.ts +3 -0
  42. package/dist/components/Wallet/WalletButton.d.ts.map +1 -0
  43. package/dist/components/Wallet/WalletButton.js +51 -0
  44. package/dist/components/Wallet/WalletButton.jsx +53 -0
  45. package/dist/components/Wallet/WalletConnectionModal.d.ts +8 -0
  46. package/dist/components/Wallet/WalletConnectionModal.d.ts.map +1 -0
  47. package/dist/components/Wallet/WalletConnectionModal.js +150 -0
  48. package/dist/components/Wallet/WalletConnectionModal.jsx +170 -0
  49. package/dist/components/Wallet/WalletProvider.d.ts +9 -0
  50. package/dist/components/Wallet/WalletProvider.d.ts.map +1 -0
  51. package/dist/components/Wallet/WalletProvider.js +70 -0
  52. package/dist/components/Wallet/WalletProvider.jsx +75 -0
  53. package/dist/components/Wallet/index.d.ts +9 -0
  54. package/dist/components/Wallet/index.d.ts.map +1 -0
  55. package/dist/components/Wallet/index.js +8 -0
  56. package/dist/components/index.d.ts +7 -0
  57. package/dist/components/index.d.ts.map +1 -0
  58. package/dist/components/index.js +6 -0
  59. package/dist/hooks/index.d.ts +10 -0
  60. package/dist/hooks/index.d.ts.map +1 -0
  61. package/dist/hooks/index.js +9 -0
  62. package/dist/hooks/useCache.d.ts +16 -0
  63. package/dist/hooks/useCache.d.ts.map +1 -0
  64. package/dist/hooks/useCache.js +67 -0
  65. package/dist/hooks/usePolling.d.ts +16 -0
  66. package/dist/hooks/usePolling.d.ts.map +1 -0
  67. package/dist/hooks/usePolling.js +79 -0
  68. package/dist/hooks/useProgram.d.ts +14 -0
  69. package/dist/hooks/useProgram.d.ts.map +1 -0
  70. package/dist/hooks/useProgram.js +88 -0
  71. package/dist/hooks/useTokenBalance.d.ts +16 -0
  72. package/dist/hooks/useTokenBalance.d.ts.map +1 -0
  73. package/dist/hooks/useTokenBalance.js +100 -0
  74. package/dist/hooks/useVaults.d.ts +23 -0
  75. package/dist/hooks/useVaults.d.ts.map +1 -0
  76. package/dist/hooks/useVaults.js +98 -0
  77. package/dist/index.d.ts +12 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +17 -0
  80. package/dist/services/index.d.ts +7 -0
  81. package/dist/services/index.d.ts.map +1 -0
  82. package/dist/services/index.js +6 -0
  83. package/dist/services/reconciliationService.d.ts +76 -0
  84. package/dist/services/reconciliationService.d.ts.map +1 -0
  85. package/dist/services/reconciliationService.js +216 -0
  86. package/dist/services/syncService.d.ts +51 -0
  87. package/dist/services/syncService.d.ts.map +1 -0
  88. package/dist/services/syncService.js +218 -0
  89. package/dist/types/index.d.ts +201 -0
  90. package/dist/types/index.d.ts.map +1 -0
  91. package/dist/types/index.js +1 -0
  92. package/dist/utils/cacheManager.d.ts +73 -0
  93. package/dist/utils/cacheManager.d.ts.map +1 -0
  94. package/dist/utils/cacheManager.js +232 -0
  95. package/dist/utils/errorHandler.d.ts +76 -0
  96. package/dist/utils/errorHandler.d.ts.map +1 -0
  97. package/dist/utils/errorHandler.js +267 -0
  98. package/dist/utils/index.d.ts +12 -0
  99. package/dist/utils/index.d.ts.map +1 -0
  100. package/dist/utils/index.js +11 -0
  101. package/dist/utils/performanceMonitor.d.ts +75 -0
  102. package/dist/utils/performanceMonitor.d.ts.map +1 -0
  103. package/dist/utils/performanceMonitor.js +197 -0
  104. package/dist/utils/rpcRetry.d.ts +12 -0
  105. package/dist/utils/rpcRetry.d.ts.map +1 -0
  106. package/dist/utils/rpcRetry.js +47 -0
  107. package/dist/utils/supabase.d.ts +198 -0
  108. package/dist/utils/supabase.d.ts.map +1 -0
  109. package/dist/utils/supabase.js +50 -0
  110. package/dist/utils/toastService.d.ts +52 -0
  111. package/dist/utils/toastService.d.ts.map +1 -0
  112. package/dist/utils/toastService.js +139 -0
  113. package/dist/utils/tokenUtils.d.ts +33 -0
  114. package/dist/utils/tokenUtils.d.ts.map +1 -0
  115. package/dist/utils/tokenUtils.js +66 -0
  116. package/dist/utils/validation.d.ts +35 -0
  117. package/dist/utils/validation.d.ts.map +1 -0
  118. package/dist/utils/validation.js +83 -0
  119. package/package.json +45 -0
  120. package/src/components/Common/ErrorBoundary.tsx +135 -0
  121. package/src/components/Common/ErrorMessage.tsx +52 -0
  122. package/src/components/Common/Loading.tsx +56 -0
  123. package/src/components/Common/LoadingIndicator.tsx +143 -0
  124. package/src/components/Common/ProgramStatus.tsx +37 -0
  125. package/src/components/Common/Skeleton.tsx +83 -0
  126. package/src/components/Common/SkeletonScreen.tsx +166 -0
  127. package/src/components/Common/index.ts +10 -0
  128. package/src/components/Wallet/TransactionStatus.tsx +138 -0
  129. package/src/components/Wallet/WalletBalance.tsx +94 -0
  130. package/src/components/Wallet/WalletButton.tsx +65 -0
  131. package/src/components/Wallet/WalletConnectionModal.tsx +193 -0
  132. package/src/components/Wallet/WalletProvider.tsx +104 -0
  133. package/src/components/Wallet/index.ts +8 -0
  134. package/src/components/index.ts +6 -0
  135. package/src/hooks/index.ts +10 -0
  136. package/src/hooks/useCache.ts +87 -0
  137. package/src/hooks/usePolling.ts +98 -0
  138. package/src/hooks/useProgram.ts +93 -0
  139. package/src/hooks/useTokenBalance.ts +113 -0
  140. package/src/hooks/useVaults.ts +122 -0
  141. package/src/index.ts +23 -0
  142. package/src/services/index.ts +6 -0
  143. package/src/services/reconciliationService.ts +246 -0
  144. package/src/services/syncService.ts +238 -0
  145. package/src/types/index.ts +233 -0
  146. package/src/utils/cacheManager.ts +286 -0
  147. package/src/utils/errorHandler.ts +336 -0
  148. package/src/utils/index.ts +12 -0
  149. package/src/utils/performanceMonitor.ts +222 -0
  150. package/src/utils/rpcRetry.ts +55 -0
  151. package/src/utils/supabase.ts +253 -0
  152. package/src/utils/toastService.ts +166 -0
  153. package/src/utils/tokenUtils.ts +75 -0
  154. package/src/utils/validation.ts +107 -0
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Loading Indicator Component
3
+ * Displays loading feedback appropriate to context
4
+ * Requirements: 5.2, 5.5
5
+ */
6
+ import React from 'react';
7
+ import styled, { keyframes } from 'styled-components';
8
+
9
+ export type LoadingType = 'spinner' | 'progress' | 'dots';
10
+ export type LoadingSize = 'small' | 'medium' | 'large';
11
+
12
+ interface LoadingIndicatorProps {
13
+ type: LoadingType;
14
+ size?: LoadingSize;
15
+ message?: string;
16
+ progress?: number; // 0-100 for progress type
17
+ }
18
+
19
+ const spin = keyframes`
20
+ to { transform: rotate(360deg); }
21
+ `;
22
+
23
+ const dots = keyframes`
24
+ 0%, 20% {
25
+ content: '.';
26
+ }
27
+ 40% {
28
+ content: '..';
29
+ }
30
+ 60%, 100% {
31
+ content: '...';
32
+ }
33
+ `;
34
+
35
+ const Container = styled.div`
36
+ display: flex;
37
+ flex-direction: column;
38
+ align-items: center;
39
+ justify-content: center;
40
+ gap: var(--spacing-md, 1rem);
41
+ padding: var(--spacing-lg, 1.5rem);
42
+ `;
43
+
44
+ const Spinner = styled.div<{ size: LoadingSize }>`
45
+ width: ${props => {
46
+ switch (props.size) {
47
+ case 'small': return '24px';
48
+ case 'large': return '64px';
49
+ default: return '48px';
50
+ }
51
+ }};
52
+ height: ${props => {
53
+ switch (props.size) {
54
+ case 'small': return '24px';
55
+ case 'large': return '64px';
56
+ default: return '48px';
57
+ }
58
+ }};
59
+ border: 4px solid var(--color-border, #e5e7eb);
60
+ border-top-color: var(--color-primary, #6a8102);
61
+ border-radius: 50%;
62
+ animation: ${spin} 1s linear infinite;
63
+ `;
64
+
65
+ const ProgressBarContainer = styled.div`
66
+ width: 100%;
67
+ max-width: 300px;
68
+ height: 8px;
69
+ background: var(--color-border, #e5e7eb);
70
+ border-radius: var(--border-radius-md, 0.375rem);
71
+ overflow: hidden;
72
+ `;
73
+
74
+ const ProgressBar = styled.div<{ progress: number }>`
75
+ height: 100%;
76
+ width: ${props => props.progress}%;
77
+ background: linear-gradient(90deg, var(--color-primary, #6a8102), var(--color-secondary, #ffc107));
78
+ border-radius: var(--border-radius-md, 0.375rem);
79
+ transition: width 0.3s ease;
80
+ `;
81
+
82
+ const Dots = styled.div`
83
+ font-size: 1.5rem;
84
+ color: var(--color-primary, #6a8102);
85
+
86
+ &::after {
87
+ content: '.';
88
+ animation: ${dots} 1.5s steps(4, end) infinite;
89
+ }
90
+ `;
91
+
92
+ const Message = styled.p`
93
+ color: var(--color-text-secondary, #6b7280);
94
+ font-size: 0.875rem;
95
+ margin: 0;
96
+ text-align: center;
97
+ `;
98
+
99
+ const ProgressText = styled.span`
100
+ color: var(--color-text-secondary, #6b7280);
101
+ font-size: 0.75rem;
102
+ font-weight: 600;
103
+ margin-top: var(--spacing-xs, 0.25rem);
104
+ `;
105
+
106
+ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
107
+ type,
108
+ size = 'medium',
109
+ message,
110
+ progress,
111
+ }) => {
112
+ const renderIndicator = () => {
113
+ switch (type) {
114
+ case 'spinner':
115
+ return <Spinner size={size} />;
116
+
117
+ case 'progress':
118
+ return (
119
+ <div style={{ width: '100%', maxWidth: '300px' }}>
120
+ <ProgressBarContainer>
121
+ <ProgressBar progress={progress || 0} />
122
+ </ProgressBarContainer>
123
+ {progress !== undefined && (
124
+ <ProgressText>{Math.round(progress)}%</ProgressText>
125
+ )}
126
+ </div>
127
+ );
128
+
129
+ case 'dots':
130
+ return <Dots />;
131
+
132
+ default:
133
+ return null;
134
+ }
135
+ };
136
+
137
+ return (
138
+ <Container>
139
+ {renderIndicator()}
140
+ {message && <Message>{message}</Message>}
141
+ </Container>
142
+ );
143
+ };
@@ -0,0 +1,37 @@
1
+ import React, { useEffect } from 'react';
2
+ import styled from 'styled-components';
3
+ import { useProgram } from '../../hooks/useProgram';
4
+ import { ErrorMessage } from './ErrorMessage';
5
+ import toast from 'react-hot-toast';
6
+
7
+ export const ProgramStatus: React.FC = () => {
8
+ const { program, idl, idlError, programId } = useProgram();
9
+ const [showError, setShowError] = React.useState(false);
10
+
11
+ useEffect(() => {
12
+ if (idlError) {
13
+ setShowError(true);
14
+ toast.error(`Failed to load program: ${idlError}`, {
15
+ duration: 5000,
16
+ });
17
+ }
18
+ }, [idlError]);
19
+
20
+ // Don't show anything if program is loading or ready
21
+ if (!idlError) {
22
+ return null;
23
+ }
24
+
25
+ return (
26
+ <Container>
27
+ <ErrorMessage
28
+ message={`Failed to load program contract. Please refresh the page. Error: ${idlError}`}
29
+ onRetry={() => window.location.reload()}
30
+ />
31
+ </Container>
32
+ );
33
+ };
34
+
35
+ const Container = styled.div`
36
+ margin: var(--spacing-md, 1rem) 0;
37
+ `;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Skeleton Screen Components
3
+ * Requirements: 11.3 - Loading states with skeleton screens for better perceived performance
4
+ */
5
+ import React from 'react';
6
+ import styled, { keyframes } from 'styled-components';
7
+
8
+ const shimmer = keyframes`
9
+ 0% {
10
+ background-position: -1000px 0;
11
+ }
12
+ 100% {
13
+ background-position: 1000px 0;
14
+ }
15
+ `;
16
+
17
+ const SkeletonBase = styled.div<{ width?: string; height?: string }>`
18
+ background: linear-gradient(
19
+ 90deg,
20
+ var(--color-surface, #f9fafb) 0%,
21
+ var(--color-border, #e5e7eb) 50%,
22
+ var(--color-surface, #f9fafb) 100%
23
+ );
24
+ background-size: 1000px 100%;
25
+ animation: ${shimmer} 2s infinite;
26
+ border-radius: var(--border-radius-md, 0.375rem);
27
+ width: ${props => props.width || '100%'};
28
+ height: ${props => props.height || '20px'};
29
+ `;
30
+
31
+ export const SkeletonCard = styled(SkeletonBase)`
32
+ height: 200px;
33
+ margin-bottom: var(--spacing-md, 1rem);
34
+ `;
35
+
36
+ export const SkeletonText = styled(SkeletonBase)`
37
+ height: 16px;
38
+ margin-bottom: var(--spacing-xs, 0.25rem);
39
+
40
+ &:last-child {
41
+ margin-bottom: 0;
42
+ }
43
+ `;
44
+
45
+ export const SkeletonTitle = styled(SkeletonBase)`
46
+ height: 24px;
47
+ width: 60%;
48
+ margin-bottom: var(--spacing-md, 1rem);
49
+ `;
50
+
51
+ export const SkeletonButton = styled(SkeletonBase)`
52
+ height: 40px;
53
+ width: 120px;
54
+ border-radius: var(--border-radius-md, 0.375rem);
55
+ `;
56
+
57
+ export const SkeletonAvatar = styled(SkeletonBase)`
58
+ width: 64px;
59
+ height: 64px;
60
+ border-radius: 50%;
61
+ `;
62
+
63
+ export const VaultCardSkeleton: React.FC = () => (
64
+ <SkeletonCard />
65
+ );
66
+
67
+ export const StatsSkeleton: React.FC = () => (
68
+ <>
69
+ <SkeletonTitle />
70
+ <SkeletonText width="100%" />
71
+ <SkeletonText width="80%" />
72
+ <SkeletonText width="90%" />
73
+ </>
74
+ );
75
+
76
+ export const ProfileSkeleton: React.FC = () => (
77
+ <>
78
+ <SkeletonAvatar width="80px" height="80px" />
79
+ <SkeletonTitle width="40%" />
80
+ <SkeletonText width="60%" />
81
+ <SkeletonText width="50%" />
82
+ </>
83
+ );
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Skeleton Screen Component
3
+ * Loading placeholder that mimics content structure
4
+ * Requirements: 5.1
5
+ */
6
+ import React from 'react';
7
+ import styled, { keyframes } from 'styled-components';
8
+
9
+ export type SkeletonVariant = 'card' | 'table' | 'chart' | 'list' | 'text';
10
+ export type SkeletonAnimation = 'pulse' | 'wave' | 'none';
11
+
12
+ interface SkeletonScreenProps {
13
+ variant: SkeletonVariant;
14
+ count?: number;
15
+ height?: string;
16
+ width?: string;
17
+ animation?: SkeletonAnimation;
18
+ }
19
+
20
+ const pulse = keyframes`
21
+ 0%, 100% {
22
+ opacity: 1;
23
+ }
24
+ 50% {
25
+ opacity: 0.5;
26
+ }
27
+ `;
28
+
29
+ const wave = keyframes`
30
+ 0% {
31
+ transform: translateX(-100%);
32
+ }
33
+ 50% {
34
+ transform: translateX(100%);
35
+ }
36
+ 100% {
37
+ transform: translateX(100%);
38
+ }
39
+ `;
40
+
41
+ interface SkeletonBaseProps {
42
+ $animation?: SkeletonAnimation;
43
+ $height?: string;
44
+ $width?: string;
45
+ }
46
+
47
+ const SkeletonBase = styled.div<SkeletonBaseProps>`
48
+ background: linear-gradient(
49
+ 90deg,
50
+ var(--color-border, #e5e7eb) 0%,
51
+ var(--color-surface, #f9fafb) 50%,
52
+ var(--color-border, #e5e7eb) 100%
53
+ );
54
+ background-size: 200% 100%;
55
+ border-radius: var(--border-radius-md, 0.375rem);
56
+
57
+ ${props => {
58
+ switch (props.$animation) {
59
+ case 'pulse':
60
+ return `animation: ${pulse} 1.5s ease-in-out infinite;`;
61
+ case 'wave':
62
+ return `animation: ${wave} 1.5s ease-in-out infinite;`;
63
+ default:
64
+ return '';
65
+ }
66
+ }}
67
+ `;
68
+
69
+ const CardSkeleton = styled(SkeletonBase)`
70
+ height: ${props => props.$height || '200px'};
71
+ width: ${props => props.$width || '100%'};
72
+ margin-bottom: var(--spacing-md, 1rem);
73
+ `;
74
+
75
+ const TableSkeleton = styled.div`
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: var(--spacing-sm, 0.5rem);
79
+ `;
80
+
81
+ const TableRowSkeleton = styled(SkeletonBase)`
82
+ height: ${props => props.$height || '48px'};
83
+ width: 100%;
84
+ `;
85
+
86
+ const ChartSkeleton = styled(SkeletonBase)`
87
+ height: ${props => props.$height || '300px'};
88
+ width: ${props => props.$width || '100%'};
89
+ border-radius: var(--border-radius-lg, 0.5rem);
90
+ `;
91
+
92
+ const ListSkeleton = styled.div`
93
+ display: flex;
94
+ flex-direction: column;
95
+ gap: var(--spacing-md, 1rem);
96
+ `;
97
+
98
+ const ListItemSkeleton = styled(SkeletonBase)`
99
+ height: ${props => props.$height || '60px'};
100
+ width: 100%;
101
+ `;
102
+
103
+ const TextSkeleton = styled(SkeletonBase)`
104
+ height: ${props => props.$height || '1em'};
105
+ width: ${props => props.$width || '100%'};
106
+ margin-bottom: var(--spacing-xs, 0.25rem);
107
+
108
+ &:last-child {
109
+ margin-bottom: 0;
110
+ }
111
+ `;
112
+
113
+ export const SkeletonScreen: React.FC<SkeletonScreenProps> = ({
114
+ variant,
115
+ count = 1,
116
+ height,
117
+ width,
118
+ animation = 'pulse',
119
+ }) => {
120
+ const renderSkeleton = () => {
121
+ switch (variant) {
122
+ case 'card':
123
+ return Array.from({ length: count }).map((_, i) => (
124
+ <CardSkeleton key={i} $animation={animation} $height={height} $width={width} />
125
+ ));
126
+
127
+ case 'table':
128
+ return (
129
+ <TableSkeleton>
130
+ {Array.from({ length: count }).map((_, i) => (
131
+ <TableRowSkeleton key={i} $animation={animation} $height={height} />
132
+ ))}
133
+ </TableSkeleton>
134
+ );
135
+
136
+ case 'chart':
137
+ return (
138
+ <ChartSkeleton $animation={animation} $height={height} $width={width} />
139
+ );
140
+
141
+ case 'list':
142
+ return (
143
+ <ListSkeleton>
144
+ {Array.from({ length: count }).map((_, i) => (
145
+ <ListItemSkeleton key={i} $animation={animation} $height={height} />
146
+ ))}
147
+ </ListSkeleton>
148
+ );
149
+
150
+ case 'text':
151
+ return Array.from({ length: count }).map((_, i) => (
152
+ <TextSkeleton
153
+ key={i}
154
+ $animation={animation}
155
+ $height={height}
156
+ $width={i === count - 1 ? '80%' : width} // Last line shorter
157
+ />
158
+ ));
159
+
160
+ default:
161
+ return null;
162
+ }
163
+ };
164
+
165
+ return <>{renderSkeleton()}</>;
166
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Common Components Index
3
+ */
4
+ export * from './ErrorBoundary';
5
+ export * from './ErrorMessage';
6
+ export * from './Loading';
7
+ export * from './LoadingIndicator';
8
+ export * from './Skeleton';
9
+ export * from './SkeletonScreen';
10
+ export * from './ProgramStatus';
@@ -0,0 +1,138 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ const StatusContainer = styled.div<{ status: 'pending' | 'confirmed' | 'failed' }>`
5
+ display: flex;
6
+ align-items: center;
7
+ gap: 0.75rem;
8
+ padding: 1rem;
9
+ background: var(--color-surface, #f9fafb);
10
+ border: 1px solid ${props => {
11
+ switch (props.status) {
12
+ case 'confirmed': return 'var(--color-success, #10b981)';
13
+ case 'failed': return 'var(--color-error, #dc3545)';
14
+ default: return 'var(--color-primary, #6a8102)';
15
+ }
16
+ }};
17
+ border-radius: 0.5rem;
18
+ animation: slideIn 0.3s ease-out;
19
+
20
+ @keyframes slideIn {
21
+ from {
22
+ transform: translateX(100%);
23
+ opacity: 0;
24
+ }
25
+ to {
26
+ transform: translateX(0);
27
+ opacity: 1;
28
+ }
29
+ }
30
+ `;
31
+
32
+ const StatusIcon = styled.div<{ status: 'pending' | 'confirmed' | 'failed' }>`
33
+ width: 24px;
34
+ height: 24px;
35
+ display: flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ font-size: 1.25rem;
39
+
40
+ ${props => props.status === 'pending' && `
41
+ animation: spin 1s linear infinite;
42
+ @keyframes spin {
43
+ to { transform: rotate(360deg); }
44
+ }
45
+ `}
46
+ `;
47
+
48
+ const StatusContent = styled.div`
49
+ flex: 1;
50
+ display: flex;
51
+ flex-direction: column;
52
+ gap: 0.25rem;
53
+ `;
54
+
55
+ const StatusTitle = styled.span`
56
+ font-weight: 600;
57
+ color: var(--color-text, #1a1a1a);
58
+ font-size: 0.875rem;
59
+ `;
60
+
61
+ const StatusMessage = styled.span`
62
+ font-size: 0.75rem;
63
+ color: var(--color-text-secondary, #6b7280);
64
+ `;
65
+
66
+ const SignatureLink = styled.a`
67
+ font-size: 0.75rem;
68
+ color: var(--color-primary, #6a8102);
69
+ text-decoration: none;
70
+ font-family: monospace;
71
+
72
+ &:hover {
73
+ text-decoration: underline;
74
+ }
75
+ `;
76
+
77
+ interface TransactionStatusProps {
78
+ status: 'pending' | 'confirmed' | 'failed';
79
+ message: string;
80
+ signature?: string;
81
+ explorerUrl?: string;
82
+ network?: string;
83
+ }
84
+
85
+ const TransactionStatus: React.FC<TransactionStatusProps> = ({
86
+ status,
87
+ message,
88
+ signature,
89
+ explorerUrl = 'https://explorer.solana.com/tx',
90
+ network
91
+ }) => {
92
+ const getIcon = () => {
93
+ switch (status) {
94
+ case 'confirmed':
95
+ return '✓';
96
+ case 'failed':
97
+ return '✗';
98
+ default:
99
+ return '⟳';
100
+ }
101
+ };
102
+
103
+ const getTitle = () => {
104
+ switch (status) {
105
+ case 'confirmed':
106
+ return 'Transaction Confirmed';
107
+ case 'failed':
108
+ return 'Transaction Failed';
109
+ default:
110
+ return 'Transaction Pending';
111
+ }
112
+ };
113
+
114
+ const networkParam = network ||
115
+ (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_SOLANA_NETWORK) ||
116
+ 'devnet';
117
+
118
+ return (
119
+ <StatusContainer status={status}>
120
+ <StatusIcon status={status}>{getIcon()}</StatusIcon>
121
+ <StatusContent>
122
+ <StatusTitle>{getTitle()}</StatusTitle>
123
+ <StatusMessage>{message}</StatusMessage>
124
+ {signature && status === 'confirmed' && (
125
+ <SignatureLink
126
+ href={`${explorerUrl}/${signature}?cluster=${networkParam}`}
127
+ target="_blank"
128
+ rel="noopener noreferrer"
129
+ >
130
+ View on Explorer →
131
+ </SignatureLink>
132
+ )}
133
+ </StatusContent>
134
+ </StatusContainer>
135
+ );
136
+ };
137
+
138
+ export default TransactionStatus;
@@ -0,0 +1,94 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import styled from 'styled-components';
3
+ import { useWallet, useConnection } from '@solana/wallet-adapter-react';
4
+ import { LAMPORTS_PER_SOL } from '@solana/web3.js';
5
+
6
+ const BalanceContainer = styled.div`
7
+ display: flex;
8
+ flex-direction: column;
9
+ gap: 0.5rem;
10
+ padding: 1rem;
11
+ background: var(--color-surface, #f9fafb);
12
+ border: 1px solid var(--color-primary, #6a8102);
13
+ border-radius: 0.5rem;
14
+ `;
15
+
16
+ const BalanceLabel = styled.span`
17
+ font-size: 0.75rem;
18
+ color: var(--color-text-secondary, #6b7280);
19
+ text-transform: uppercase;
20
+ letter-spacing: 0.05em;
21
+ `;
22
+
23
+ const BalanceValue = styled.span`
24
+ font-size: 1.25rem;
25
+ font-weight: 700;
26
+ color: var(--color-primary, #6a8102);
27
+ font-family: monospace;
28
+ `;
29
+
30
+ const LoadingSpinner = styled.div`
31
+ width: 16px;
32
+ height: 16px;
33
+ border: 2px solid var(--color-primary, #6a8102);
34
+ border-top-color: transparent;
35
+ border-radius: 50%;
36
+ animation: spin 1s linear infinite;
37
+
38
+ @keyframes spin {
39
+ to { transform: rotate(360deg); }
40
+ }
41
+ `;
42
+
43
+ const WalletBalance: React.FC = () => {
44
+ const { publicKey, connected } = useWallet();
45
+ const { connection } = useConnection();
46
+ const [balance, setBalance] = useState<number | null>(null);
47
+ const [loading, setLoading] = useState(false);
48
+
49
+ useEffect(() => {
50
+ if (!connected || !publicKey) {
51
+ setBalance(null);
52
+ return;
53
+ }
54
+
55
+ const fetchBalance = async () => {
56
+ try {
57
+ setLoading(true);
58
+ const lamports = await connection.getBalance(publicKey);
59
+ setBalance(lamports / LAMPORTS_PER_SOL);
60
+ } catch (error) {
61
+ console.error('Failed to fetch balance:', error);
62
+ setBalance(null);
63
+ } finally {
64
+ setLoading(false);
65
+ }
66
+ };
67
+
68
+ fetchBalance();
69
+
70
+ // Poll for balance updates every 10 seconds
71
+ const interval = setInterval(fetchBalance, 10000);
72
+
73
+ return () => clearInterval(interval);
74
+ }, [connected, publicKey, connection]);
75
+
76
+ if (!connected) {
77
+ return null;
78
+ }
79
+
80
+ return (
81
+ <BalanceContainer>
82
+ <BalanceLabel>Wallet Balance</BalanceLabel>
83
+ {loading ? (
84
+ <LoadingSpinner />
85
+ ) : (
86
+ <BalanceValue>
87
+ {balance !== null ? `${balance.toFixed(4)} SOL` : '—'}
88
+ </BalanceValue>
89
+ )}
90
+ </BalanceContainer>
91
+ );
92
+ };
93
+
94
+ export default WalletBalance;