tapquest-ui-yeulamvietnam 2.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 (188) hide show
  1. package/example/README.md +50 -0
  2. package/example/eslint.config.js +28 -0
  3. package/example/index.html +13 -0
  4. package/example/package.json +35 -0
  5. package/example/public/vite.svg +1 -0
  6. package/example/src/App.css +8 -0
  7. package/example/src/App.tsx +62 -0
  8. package/example/src/Card/index.tsx +15 -0
  9. package/example/src/Icons/CircleArrow.tsx +8 -0
  10. package/example/src/Icons/icon1.tsx +19 -0
  11. package/example/src/View/CoreComponentView/index.tsx +255 -0
  12. package/example/src/View/MapView/index.tsx +1325 -0
  13. package/example/src/View/MapView/map.html +14 -0
  14. package/example/src/assets/fonts/Kanit/Kanit-Black.ttf +0 -0
  15. package/example/src/assets/fonts/Kanit/Kanit-BlackItalic.ttf +0 -0
  16. package/example/src/assets/fonts/Kanit/Kanit-Bold.ttf +0 -0
  17. package/example/src/assets/fonts/Kanit/Kanit-BoldItalic.ttf +0 -0
  18. package/example/src/assets/fonts/Kanit/Kanit-ExtraBold.ttf +0 -0
  19. package/example/src/assets/fonts/Kanit/Kanit-ExtraBoldItalic.ttf +0 -0
  20. package/example/src/assets/fonts/Kanit/Kanit-ExtraLight.ttf +0 -0
  21. package/example/src/assets/fonts/Kanit/Kanit-ExtraLightItalic.ttf +0 -0
  22. package/example/src/assets/fonts/Kanit/Kanit-Italic.ttf +0 -0
  23. package/example/src/assets/fonts/Kanit/Kanit-Light.ttf +0 -0
  24. package/example/src/assets/fonts/Kanit/Kanit-LightItalic.ttf +0 -0
  25. package/example/src/assets/fonts/Kanit/Kanit-Medium.ttf +0 -0
  26. package/example/src/assets/fonts/Kanit/Kanit-MediumItalic.ttf +0 -0
  27. package/example/src/assets/fonts/Kanit/Kanit-Regular.ttf +0 -0
  28. package/example/src/assets/fonts/Kanit/Kanit-SemiBold.ttf +0 -0
  29. package/example/src/assets/fonts/Kanit/Kanit-SemiBoldItalic.ttf +0 -0
  30. package/example/src/assets/fonts/Kanit/Kanit-Thin.ttf +0 -0
  31. package/example/src/assets/fonts/Kanit/Kanit-ThinItalic.ttf +0 -0
  32. package/example/src/assets/fonts/Kanit/OFL.txt +93 -0
  33. package/example/src/assets/fonts/Roboto/Roboto-Black.ttf +0 -0
  34. package/example/src/assets/fonts/Roboto/Roboto-BlackItalic.ttf +0 -0
  35. package/example/src/assets/fonts/Roboto/Roboto-Bold.ttf +0 -0
  36. package/example/src/assets/fonts/Roboto/Roboto-BoldItalic.ttf +0 -0
  37. package/example/src/assets/fonts/Roboto/Roboto-ExtraBold.ttf +0 -0
  38. package/example/src/assets/fonts/Roboto/Roboto-ExtraBoldItalic.ttf +0 -0
  39. package/example/src/assets/fonts/Roboto/Roboto-ExtraLight.ttf +0 -0
  40. package/example/src/assets/fonts/Roboto/Roboto-ExtraLightItalic.ttf +0 -0
  41. package/example/src/assets/fonts/Roboto/Roboto-Italic.ttf +0 -0
  42. package/example/src/assets/fonts/Roboto/Roboto-Light.ttf +0 -0
  43. package/example/src/assets/fonts/Roboto/Roboto-LightItalic.ttf +0 -0
  44. package/example/src/assets/fonts/Roboto/Roboto-Medium.ttf +0 -0
  45. package/example/src/assets/fonts/Roboto/Roboto-MediumItalic.ttf +0 -0
  46. package/example/src/assets/fonts/Roboto/Roboto-Regular.ttf +0 -0
  47. package/example/src/assets/fonts/Roboto/Roboto-SemiBold.ttf +0 -0
  48. package/example/src/assets/fonts/Roboto/Roboto-SemiBoldItalic.ttf +0 -0
  49. package/example/src/assets/fonts/Roboto/Roboto-Thin.ttf +0 -0
  50. package/example/src/assets/fonts/Roboto/Roboto-ThinItalic.ttf +0 -0
  51. package/example/src/assets/fonts/Roboto/Roboto_Condensed-Black.ttf +0 -0
  52. package/example/src/assets/fonts/Roboto/Roboto_Condensed-BlackItalic.ttf +0 -0
  53. package/example/src/assets/fonts/Roboto/Roboto_Condensed-Bold.ttf +0 -0
  54. package/example/src/assets/fonts/Roboto/Roboto_Condensed-BoldItalic.ttf +0 -0
  55. package/example/src/assets/fonts/Roboto/Roboto_Condensed-ExtraBold.ttf +0 -0
  56. package/example/src/assets/fonts/Roboto/Roboto_Condensed-ExtraBoldItalic.ttf +0 -0
  57. package/example/src/assets/fonts/Roboto/Roboto_Condensed-ExtraLight.ttf +0 -0
  58. package/example/src/assets/fonts/Roboto/Roboto_Condensed-ExtraLightItalic.ttf +0 -0
  59. package/example/src/assets/fonts/Roboto/Roboto_Condensed-Italic.ttf +0 -0
  60. package/example/src/assets/fonts/Roboto/Roboto_Condensed-Light.ttf +0 -0
  61. package/example/src/assets/fonts/Roboto/Roboto_Condensed-LightItalic.ttf +0 -0
  62. package/example/src/assets/fonts/Roboto/Roboto_Condensed-Medium.ttf +0 -0
  63. package/example/src/assets/fonts/Roboto/Roboto_Condensed-MediumItalic.ttf +0 -0
  64. package/example/src/assets/fonts/Roboto/Roboto_Condensed-Regular.ttf +0 -0
  65. package/example/src/assets/fonts/Roboto/Roboto_Condensed-SemiBold.ttf +0 -0
  66. package/example/src/assets/fonts/Roboto/Roboto_Condensed-SemiBoldItalic.ttf +0 -0
  67. package/example/src/assets/fonts/Roboto/Roboto_Condensed-Thin.ttf +0 -0
  68. package/example/src/assets/fonts/Roboto/Roboto_Condensed-ThinItalic.ttf +0 -0
  69. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-Black.ttf +0 -0
  70. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-BlackItalic.ttf +0 -0
  71. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-Bold.ttf +0 -0
  72. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-BoldItalic.ttf +0 -0
  73. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-ExtraBold.ttf +0 -0
  74. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-ExtraBoldItalic.ttf +0 -0
  75. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-ExtraLight.ttf +0 -0
  76. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-ExtraLightItalic.ttf +0 -0
  77. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-Italic.ttf +0 -0
  78. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-Light.ttf +0 -0
  79. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-LightItalic.ttf +0 -0
  80. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-Medium.ttf +0 -0
  81. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-MediumItalic.ttf +0 -0
  82. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-Regular.ttf +0 -0
  83. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-SemiBold.ttf +0 -0
  84. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-SemiBoldItalic.ttf +0 -0
  85. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-Thin.ttf +0 -0
  86. package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-ThinItalic.ttf +0 -0
  87. package/example/src/assets/fonts/iCielBCLodestone/iCielBCLodestone.ttf +0 -0
  88. package/example/src/assets/react.svg +1 -0
  89. package/example/src/fonts.css +66 -0
  90. package/example/src/index.css +70 -0
  91. package/example/src/main.tsx +10 -0
  92. package/example/src/vite-env.d.ts +1 -0
  93. package/example/tsconfig.app.json +26 -0
  94. package/example/tsconfig.json +7 -0
  95. package/example/tsconfig.node.json +24 -0
  96. package/example/vite.config.ts +7 -0
  97. package/index.css +20 -0
  98. package/index.ts +42 -0
  99. package/package.json +44 -0
  100. package/src/components/AppbarPrimaryButton/index.tsx +56 -0
  101. package/src/components/Avatar.styled/index.tsx +8 -0
  102. package/src/components/Button.styled/index.tsx +154 -0
  103. package/src/components/Card.styled/index.tsx +26 -0
  104. package/src/components/Compound/Appbar/index.tsx +88 -0
  105. package/src/components/Compound/Header/index.tsx +40 -0
  106. package/src/components/Compound/InteractiveMap/MapSvg.tsx +608 -0
  107. package/src/components/Compound/LocationOverviewCard/index.tsx +186 -0
  108. package/src/components/Compound/MemoryCard/index.tsx +267 -0
  109. package/src/components/Compound/ProgressStep/index.tsx +54 -0
  110. package/src/components/Compound/SponsorByContainer/index.tsx +31 -0
  111. package/src/components/FormItem.styled/index.tsx +23 -0
  112. package/src/components/Icons/AppbarBg.tsx +22 -0
  113. package/src/components/Icons/ArrowCircle.tsx +8 -0
  114. package/src/components/Icons/ArrowDown.tsx +15 -0
  115. package/src/components/Icons/Camera.tsx +17 -0
  116. package/src/components/Icons/Check.tsx +7 -0
  117. package/src/components/Icons/ChevronLeft.tsx +7 -0
  118. package/src/components/Icons/ChevronRight.tsx +16 -0
  119. package/src/components/Icons/CircleAlert.tsx +9 -0
  120. package/src/components/Icons/CircleArrow.tsx +8 -0
  121. package/src/components/Icons/CircleCheck.tsx +7 -0
  122. package/src/components/Icons/CornerUpRight.tsx +7 -0
  123. package/src/components/Icons/Dart.tsx +7 -0
  124. package/src/components/Icons/Discover.tsx +16 -0
  125. package/src/components/Icons/Edit.tsx +16 -0
  126. package/src/components/Icons/Email.tsx +16 -0
  127. package/src/components/Icons/Exclaimation.tsx +7 -0
  128. package/src/components/Icons/Facebook.tsx +7 -0
  129. package/src/components/Icons/Gear.tsx +15 -0
  130. package/src/components/Icons/Gift.tsx +18 -0
  131. package/src/components/Icons/Globe.tsx +14 -0
  132. package/src/components/Icons/Home.tsx +7 -0
  133. package/src/components/Icons/Icon1.tsx +19 -0
  134. package/src/components/Icons/Icon1sm.tsx +19 -0
  135. package/src/components/Icons/Instagram.tsx +9 -0
  136. package/src/components/Icons/Link.tsx +16 -0
  137. package/src/components/Icons/LocationPin.tsx +10 -0
  138. package/src/components/Icons/Logout.tsx +15 -0
  139. package/src/components/Icons/MapMarker.tsx +8 -0
  140. package/src/components/Icons/Marker/MarkerRed.tsx +17 -0
  141. package/src/components/Icons/Menu.tsx +11 -0
  142. package/src/components/Icons/Mission.tsx +17 -0
  143. package/src/components/Icons/Moment.tsx +18 -0
  144. package/src/components/Icons/Phone.tsx +15 -0
  145. package/src/components/Icons/Pin.tsx +8 -0
  146. package/src/components/Icons/PinCircle.tsx +17 -0
  147. package/src/components/Icons/PinOutlined.tsx +7 -0
  148. package/src/components/Icons/Profile.tsx +15 -0
  149. package/src/components/Icons/ProfileGift.tsx +23 -0
  150. package/src/components/Icons/ProgressBar.tsx +20 -0
  151. package/src/components/Icons/ProgressBarInner.tsx +44 -0
  152. package/src/components/Icons/SealCheckIcon.tsx +18 -0
  153. package/src/components/Icons/Search.tsx +7 -0
  154. package/src/components/Icons/SendMessage.tsx +7 -0
  155. package/src/components/Icons/Share.tsx +14 -0
  156. package/src/components/Icons/ShareMemoryBadge.tsx +11 -0
  157. package/src/components/Icons/ShieldWarningIcon.tsx +18 -0
  158. package/src/components/Icons/Spinner.tsx +18 -0
  159. package/src/components/Icons/Step.tsx +7 -0
  160. package/src/components/Icons/StepChecked.tsx +8 -0
  161. package/src/components/Icons/StepLine.tsx +7 -0
  162. package/src/components/Icons/Telegram.tsx +7 -0
  163. package/src/components/Icons/Trash.tsx +7 -0
  164. package/src/components/Icons/User.tsx +15 -0
  165. package/src/components/Icons/XIcon.tsx +8 -0
  166. package/src/components/Icons/Zalo.tsx +23 -0
  167. package/src/components/Icons/index.tsx +66 -0
  168. package/src/components/Image.styled/index.tsx +35 -0
  169. package/src/components/Input.styled/index.tsx +34 -0
  170. package/src/components/InputPassword/index.tsx +34 -0
  171. package/src/components/InputSearch.styled/index.tsx +18 -0
  172. package/src/components/Loader.styled/index.tsx +26 -0
  173. package/src/components/Modal.styled/index.tsx +63 -0
  174. package/src/components/ProcessBar.styled/index.tsx +26 -0
  175. package/src/components/ProgressCircular.styled/index.tsx +61 -0
  176. package/src/components/SVGs/NoResult.tsx +62 -0
  177. package/src/components/SVGs/index.tsx +3 -0
  178. package/src/components/Select.styled/index.tsx +32 -0
  179. package/src/components/Tabs.styled/index.tsx +22 -0
  180. package/src/components/TextArea.styled/index.tsx +34 -0
  181. package/src/components/Typography.styled/index.tsx +419 -0
  182. package/src/helpers/index.ts +175 -0
  183. package/src/hooks/useHTMLToCanvas.tsx +115 -0
  184. package/src/hooks/useInteractiveMap.tsx +659 -0
  185. package/src/types/type.d.ts +9 -0
  186. package/tsconfig.json +33 -0
  187. package/tsconfig.node.json +12 -0
  188. package/tsup.config.ts +24 -0
@@ -0,0 +1,1325 @@
1
+ import React, { useState, useEffect, useRef, useMemo, use } from 'react';
2
+ import useInteractiveMap from '../../../../src/hooks/useInteractiveMap';
3
+ import useHTMLToCanvas from '../../../../src/hooks/useHTMLToCanvas';
4
+ import styled from 'styled-components';
5
+
6
+ // Updated styled components for better screen fitting and overflow handling
7
+ const Container = styled.div`
8
+ display: flex;
9
+ flex-direction: column;
10
+ width: 100%;
11
+ height: 80vh;
12
+ overflow: hidden;
13
+
14
+ @media (min-width: 768px) {
15
+ flex-direction: row;
16
+ }
17
+ `;
18
+
19
+ const MapContainer = styled.div`
20
+ width: 100%;
21
+ height: 30vh;
22
+ border: 1px solid #e0e0e0;
23
+ border-radius: 8px;
24
+ overflow: hidden;
25
+ position: relative;
26
+
27
+ @media (min-width: 768px) {
28
+ flex: 1;
29
+ height: 100%;
30
+ border-radius: 0;
31
+ border: none;
32
+ border-right: 1px solid #e0e0e0;
33
+ }
34
+ `;
35
+
36
+ const DebugPanel = styled.div`
37
+ width: 100%;
38
+ height: 50vh;
39
+ padding: 16px;
40
+ background-color: #f5f5f5;
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: 16px;
44
+ overflow-y: auto;
45
+
46
+ @media (min-width: 768px) {
47
+ width: 350px;
48
+ height: 100%;
49
+ max-height: 100%;
50
+ }
51
+ `;
52
+
53
+ const DebugPanelContent = styled.div`
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: 16px;
57
+ padding-bottom: 20px;
58
+ `;
59
+
60
+ const FormGroup = styled.div`
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 8px;
64
+ `;
65
+
66
+ const Label = styled.label`
67
+ font-size: 14px;
68
+ font-weight: 600;
69
+ color: black;
70
+ text-align: left;
71
+ `;
72
+
73
+ const Input = styled.input`
74
+ padding: 8px;
75
+ border: 1px solid #ccc;
76
+ border-radius: 4px;
77
+ width: 100%;
78
+ `;
79
+
80
+ const Button = styled.button`
81
+ padding: 10px 16px;
82
+ background-color: #4a90e2;
83
+ color: white;
84
+ border: none;
85
+ border-radius: 4px;
86
+ cursor: pointer;
87
+ font-weight: 600;
88
+ transition: background-color 0.2s;
89
+ width: 100%;
90
+
91
+ &:hover {
92
+ background-color: #3a80d2;
93
+ }
94
+
95
+ &:disabled {
96
+ background-color: #cccccc;
97
+ cursor: not-allowed;
98
+ }
99
+ `;
100
+
101
+ const Select = styled.select`
102
+ padding: 8px;
103
+ border: 1px solid #ccc;
104
+ border-radius: 4px;
105
+ width: 100%;
106
+ `;
107
+
108
+ const Divider = styled.hr`
109
+ border: 0;
110
+ height: 1px;
111
+ background-color: #e0e0e0;
112
+ margin: 8px 0;
113
+ width: 100%;
114
+ `;
115
+
116
+ const ColorInputContainer = styled.div`
117
+ display: flex;
118
+ align-items: center;
119
+ width: 100%;
120
+ gap: 8px;
121
+ `;
122
+
123
+ const ColorPickerWrapper = styled.div`
124
+ position: relative;
125
+ width: 100%;
126
+ `;
127
+
128
+ const ColorInput = styled.input`
129
+ -webkit-appearance: none;
130
+ -moz-appearance: none;
131
+ appearance: none;
132
+ width: 100%;
133
+ height: 40px;
134
+ padding: 0;
135
+ border: 1px solid #ccc;
136
+ border-radius: 4px;
137
+ cursor: pointer;
138
+ background-color: transparent;
139
+
140
+ &::-webkit-color-swatch-wrapper {
141
+ padding: 0;
142
+ }
143
+ &::-webkit-color-swatch {
144
+ border: none;
145
+ border-radius: 3px;
146
+ }
147
+ &::-moz-color-swatch {
148
+ border: none;
149
+ border-radius: 3px;
150
+ }
151
+ `;
152
+
153
+ const HexInput = styled.input`
154
+ padding: 8px;
155
+ border: 1px solid #ccc;
156
+ border-radius: 4px;
157
+ width: 100px;
158
+ font-family: monospace;
159
+ text-transform: uppercase;
160
+ `;
161
+
162
+ const SectionTitle = styled.h2`
163
+ font-size: 18px;
164
+ margin: 0;
165
+ padding: 0;
166
+ `;
167
+
168
+ const ButtonGroup = styled.div`
169
+ display: grid;
170
+ grid-template-columns: 1fr 1fr;
171
+ gap: 8px;
172
+ width: 100%;
173
+
174
+ @media (max-width: 767px) {
175
+ grid-template-columns: 1fr;
176
+ }
177
+ `;
178
+
179
+ // Add a new section for marker controls
180
+ const MarkerSection = styled.div`
181
+ display: flex;
182
+ flex-direction: column;
183
+ gap: 16px;
184
+ width: 100%;
185
+ `;
186
+
187
+ const CoordinateInputs = styled.div`
188
+ display: flex;
189
+ gap: 8px;
190
+ width: 100%;
191
+ `;
192
+
193
+ const CoordinateInput = styled.input`
194
+ padding: 8px;
195
+ border: 1px solid #ccc;
196
+ border-radius: 4px;
197
+ width: 100%;
198
+ `;
199
+
200
+ const LabelSection = styled.div`
201
+ display: flex;
202
+ flex-direction: column;
203
+ gap: 16px;
204
+ width: 100%;
205
+ `;
206
+
207
+ const LabelOptions = styled.div`
208
+ display: grid;
209
+ grid-template-columns: 1fr 1fr;
210
+ gap: 8px;
211
+ width: 100%;
212
+ `;
213
+
214
+ const PreviewImage = styled.div`
215
+ width: 100%;
216
+ margin-top: 16px;
217
+ border: 1px solid #e0e0e0;
218
+ border-radius: 4px;
219
+ overflow: hidden;
220
+
221
+ img {
222
+ width: 100%;
223
+ height: auto;
224
+ display: block;
225
+ }
226
+ `;
227
+
228
+ // List of Vietnam provinces for the dropdown
229
+ const vietnamProvinces = [
230
+ { id: 'ha-noi', name: 'Hà Nội' },
231
+ { id: 'ho-chi-minh', name: 'Hồ Chí Minh' },
232
+ { id: 'da-nang', name: 'Đà Nẵng' },
233
+ { id: 'hai-phong', name: 'Hải Phòng' },
234
+ { id: 'can-tho', name: 'Cần Thơ' },
235
+ { id: 'an-giang', name: 'An Giang' },
236
+ { id: 'ba-ria-vung-tau', name: 'Bà Rịa - Vũng Tàu' },
237
+ { id: 'bac-giang', name: 'Bắc Giang' },
238
+ { id: 'bac-kan', name: 'Bắc Kạn' },
239
+ { id: 'bac-lieu', name: 'Bạc Liêu' },
240
+ { id: 'bac-ninh', name: 'Bắc Ninh' },
241
+ { id: 'ben-tre', name: 'Bến Tre' },
242
+ { id: 'binh-dinh', name: 'Bình Định' },
243
+ { id: 'binh-duong', name: 'Bình Dương' },
244
+ { id: 'binh-phuoc', name: 'Bình Phước' },
245
+ { id: 'binh-thuan', name: 'Bình Thuận' },
246
+ { id: 'ca-mau', name: 'Cà Mau' },
247
+ { id: 'cao-bang', name: 'Cao Bằng' },
248
+ { id: 'dak-lak', name: 'Đắk Lắk' },
249
+ { id: 'dak-nong', name: 'Đắk Nông' },
250
+ { id: 'dien-bien', name: 'Điện Biên' },
251
+ { id: 'dong-nai', name: 'Đồng Nai' },
252
+ { id: 'dong-thap', name: 'Đồng Tháp' },
253
+ { id: 'gia-lai', name: 'Gia Lai' },
254
+ { id: 'ha-giang', name: 'Hà Giang' },
255
+ { id: 'ha-nam', name: 'Hà Nam' },
256
+ { id: 'ha-tinh', name: 'Hà Tĩnh' },
257
+ { id: 'hai-duong', name: 'Hải Dương' },
258
+ { id: 'hau-giang', name: 'Hậu Giang' },
259
+ { id: 'hoa-binh', name: 'Hòa Bình' },
260
+ { id: 'hung-yen', name: 'Hưng Yên' },
261
+ { id: 'khanh-hoa', name: 'Khánh Hòa' },
262
+ { id: 'kien-giang', name: 'Kiên Giang' },
263
+ { id: 'kon-tum', name: 'Kon Tum' },
264
+ { id: 'lai-chau', name: 'Lai Châu' },
265
+ { id: 'lam-dong', name: 'Lâm Đồng' },
266
+ { id: 'lang-son', name: 'Lạng Sơn' },
267
+ { id: 'lao-cai', name: 'Lào Cai' },
268
+ { id: 'long-an', name: 'Long An' },
269
+ { id: 'nam-dinh', name: 'Nam Định' },
270
+ { id: 'nghe-an', name: 'Nghệ An' },
271
+ { id: 'ninh-binh', name: 'Ninh Bình' },
272
+ { id: 'ninh-thuan', name: 'Ninh Thuận' },
273
+ { id: 'phu-tho', name: 'Phú Thọ' },
274
+ { id: 'phu-yen', name: 'Phú Yên' },
275
+ { id: 'quang-binh', name: 'Quảng Bình' },
276
+ { id: 'quang-nam', name: 'Quảng Nam' },
277
+ { id: 'quang-ngai', name: 'Quảng Ngãi' },
278
+ { id: 'quang-ninh', name: 'Quảng Ninh' },
279
+ { id: 'quang-tri', name: 'Quảng Trị' },
280
+ { id: 'soc-trang', name: 'Sóc Trăng' },
281
+ { id: 'son-la', name: 'Sơn La' },
282
+ { id: 'tay-ninh', name: 'Tây Ninh' },
283
+ { id: 'thai-binh', name: 'Thái Bình' },
284
+ { id: 'thai-nguyen', name: 'Thái Nguyên' },
285
+ { id: 'thanh-hoa', name: 'Thanh Hóa' },
286
+ { id: 'thua-thien-hue', name: 'Thừa Thiên Huế' },
287
+ { id: 'tien-giang', name: 'Tiền Giang' },
288
+ { id: 'tra-vinh', name: 'Trà Vinh' },
289
+ { id: 'tuyen-quang', name: 'Tuyên Quang' },
290
+ { id: 'vinh-long', name: 'Vĩnh Long' },
291
+ { id: 'vinh-phuc', name: 'Vĩnh Phúc' },
292
+ { id: 'yen-bai', name: 'Yên Bái' },
293
+ ];
294
+
295
+ // List of Vietnam regions for the dropdown
296
+ const vietnamRegions = [
297
+ { id: 'dong-bang-song-hong', name: 'Đồng bằng sông Hồng' },
298
+ { id: 'tay-bac-bo', name: 'Tây Bắc Bộ' },
299
+ { id: 'dong-bac-bo', name: 'Đông Bắc Bộ' },
300
+ { id: 'bac-trung-bo', name: 'Bắc Trung Bộ' },
301
+ { id: 'duyen-hai-nam-trung-bo', name: 'Duyên hải Nam Trung Bộ' },
302
+ { id: 'tay-nguyen', name: 'Tây Nguyên' },
303
+ { id: 'dong-nam-bo', name: 'Đông Nam Bộ' },
304
+ { id: 'tay-nam-bo', name: 'Tây Nam Bộ' },
305
+ ];
306
+
307
+ const MapView: React.FC = () => {
308
+
309
+ const [svgImage, setSvgImage] = useState<string | null>(null);
310
+
311
+ const [svg2PngImage, setSvg2PngImage] = useState<string | null>(null);
312
+
313
+ const [templateComponent, setTemplateComponent] = useState<React.ReactNode | null>(null);
314
+
315
+ // State for map configuration
316
+ const [containerWidth, setContainerWidth] = useState('100%');
317
+ const [containerHeight, setContainerHeight] = useState('100%');
318
+ const [mapCreated, setMapCreated] = useState(false);
319
+ const [mapKey, setMapKey] = useState(0); // Key for forcing re-render
320
+
321
+ // State for province coloring
322
+ const [selectedProvince, setSelectedProvince] = useState('');
323
+ const [selectedRegion, setSelectedRegion] = useState('');
324
+ const [colorHex, setColorHex] = useState('#F4E4E4');
325
+
326
+ // Reference to the map methods
327
+ const mapMethodsRef = useRef<any>(null);
328
+
329
+ // Check if we're on mobile
330
+ const [isMobile, setIsMobile] = useState(false);
331
+
332
+ // Add state for marker functionality
333
+ const [markerX, setMarkerX] = useState<number>(100);
334
+ const [markerY, setMarkerY] = useState<number>(100);
335
+ const [markerCustomX, setMarkerCustomX] = useState<number>(100);
336
+ const [markerCustomY, setMarkerCustomY] = useState<number>(100);
337
+ const [markerId, setMarkerId] = useState<string>('marker-1');
338
+ const [markers, setMarkers] = useState<string[]>([]);
339
+ const [markerWidth, setMarkerWidth] = useState<number>(10);
340
+ const [markerHeight, setMarkerHeight] = useState<number>(10);
341
+ const [markerSvg, setMarkerSvg] = useState<string>(`<svg width="100%" height="100%" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
342
+ <rect width="9.10644" height="9.10644" fill="white" fillOpacity="0.01" style="mix-blend-mode:multiply"/>
343
+ <path d="M4.55319 0.569153C3.72329 0.570162 2.92766 0.900289 2.34082 1.48712C1.75399 2.07396 1.42386 2.86958 1.42285 3.69949C1.42187 4.37771 1.64344 5.03752 2.05356 5.57769C2.05356 5.57769 2.13893 5.68996 2.15265 5.70624L4.55319 8.53729L6.95473 5.70501C6.96737 5.68985 7.05283 5.57758 7.05283 5.57758L7.05325 5.5769C7.46313 5.03693 7.68454 4.3774 7.68353 3.69949C7.68252 2.86958 7.3524 2.07396 6.76556 1.48712C6.17873 0.900289 5.3831 0.570162 4.55319 0.569153ZM4.55319 4.8378C4.32806 4.8378 4.10798 4.77104 3.92078 4.64596C3.73359 4.52088 3.58769 4.3431 3.50154 4.1351C3.41538 3.9271 3.39284 3.69823 3.43676 3.47742C3.48068 3.25661 3.5891 3.05378 3.74829 2.89459C3.90748 2.73539 4.11031 2.62698 4.33112 2.58306C4.55193 2.53914 4.78081 2.56168 4.9888 2.64783C5.1968 2.73399 5.37458 2.87989 5.49966 3.06708C5.62474 3.25428 5.6915 3.47436 5.6915 3.69949C5.69116 4.00128 5.57112 4.29062 5.35772 4.50402C5.14432 4.71742 4.85499 4.83746 4.55319 4.8378Z" fill="#A71A1A"/>
344
+ </svg>`);
345
+ const [selectedProvinceForMarker, setSelectedProvinceForMarker] = useState<string>('');
346
+
347
+ // Add state for label settings
348
+ const [labelFontSize, setLabelFontSize] = useState<number>(10);
349
+ const [labelColor, setLabelColor] = useState<string>('#000000');
350
+ const [labelBgColor, setLabelBgColor] = useState<string>('#ffffff');
351
+ const [labelBgOpacity, setLabelBgOpacity] = useState<number>(70);
352
+
353
+ // Reference to store the remove labels function
354
+ const removeLabelsRef = useRef<(() => void) | null>(null);
355
+
356
+ // Add state for image preview
357
+ const [previewImage, setPreviewImage] = useState<string | null>(null);
358
+ const [isGeneratingImage, setIsGeneratingImage] = useState(false);
359
+
360
+ // Reference to the map container
361
+ const mapContainerRef = useRef<HTMLDivElement>(null);
362
+
363
+ // Initialize HTML to Canvas hook
364
+ const { generateCanvas } = useHTMLToCanvas();
365
+
366
+ useEffect(() => {
367
+ const checkIfMobile = () => {
368
+ setIsMobile(window.innerWidth < 768);
369
+ };
370
+
371
+ // Initial check
372
+ checkIfMobile();
373
+
374
+ // Add event listener for window resize
375
+ window.addEventListener('resize', checkIfMobile);
376
+
377
+ // Cleanup
378
+ return () => window.removeEventListener('resize', checkIfMobile);
379
+ }, []);
380
+
381
+ // Create a map of province IDs to province names
382
+ const provinceNameMap = useMemo(() => {
383
+ const map: Record<string, string> = {};
384
+ vietnamProvinces.forEach(province => {
385
+ map[province.id] = province.name;
386
+ });
387
+ return map;
388
+ }, []);
389
+
390
+ // Initialize the map with the province data
391
+ const {
392
+ mapInstance,
393
+ fillProvinceColor,
394
+ resetAllProvinceColors,
395
+ fillAreaProvinces,
396
+ fillAllProvinces,
397
+ applyRandomColors,
398
+ addMarker,
399
+ addFixedRatioMarker,
400
+ removeMarker,
401
+ removeAllMarkers,
402
+ getProvinceCenter,
403
+ addMarkerToProvince,
404
+ toggleProvinceLabels,
405
+ isProvinceLabelsVisible,
406
+ cleanup
407
+ } = useInteractiveMap({
408
+ mapId: `vietnam-map-${mapKey}`,
409
+ mapBackground: "https://storage.nomion.io/yeulamvietnam/1741956665-yeulamvietnam-map.png",
410
+ containerStyle: {
411
+ width: containerWidth,
412
+ height: containerHeight,
413
+ },
414
+ provinceData: provinceNameMap
415
+ });
416
+
417
+ // Store map methods in ref for later use
418
+ useEffect(() => {
419
+ // Only store methods in ref if mapInstance exists
420
+ if (mapInstance) {
421
+ mapMethodsRef.current = {
422
+ fillProvinceColor,
423
+ resetAllProvinceColors,
424
+ fillAreaProvinces,
425
+ fillAllProvinces,
426
+ applyRandomColors,
427
+ addMarker,
428
+ addFixedRatioMarker,
429
+ removeMarker,
430
+ removeAllMarkers,
431
+ getProvinceCenter,
432
+ addMarkerToProvince,
433
+ toggleProvinceLabels
434
+ };
435
+ }
436
+ }, [fillProvinceColor, resetAllProvinceColors, fillAreaProvinces,
437
+ fillAllProvinces, applyRandomColors, addMarker, removeMarker,
438
+ removeAllMarkers, getProvinceCenter, addMarkerToProvince,
439
+ addFixedRatioMarker,
440
+ toggleProvinceLabels, mapKey, mapInstance]);
441
+
442
+ // Handle map creation
443
+ const handleCreateMap = () => {
444
+ setMapCreated(true);
445
+ };
446
+
447
+ // Handle map reset
448
+ const handleResetMap = () => {
449
+ setMapCreated(false);
450
+ setSelectedProvince('');
451
+ setSelectedRegion('');
452
+ setColorHex('#F4E4E4');
453
+ setSvgImage(null);
454
+ setSvg2PngImage(null);
455
+
456
+ setPreviewImage(null);
457
+ setTemplateComponent(null);
458
+ setMarkers([]);
459
+
460
+ setMapKey(prevKey => prevKey + 1); // Force re-render of the map
461
+ };
462
+
463
+ // Handle province coloring
464
+ const handleColorProvince = () => {
465
+ if (!mapMethodsRef.current || !selectedProvince) return;
466
+ mapMethodsRef.current.fillProvinceColor(selectedProvince, colorHex);
467
+ };
468
+
469
+ // Handle region coloring
470
+ const handleColorRegion = () => {
471
+ if (!mapMethodsRef.current || !selectedRegion) return;
472
+ mapMethodsRef.current.fillAreaProvinces(selectedRegion, colorHex);
473
+ };
474
+
475
+ // Handle reset colors
476
+ const handleResetColors = () => {
477
+ if (!mapMethodsRef.current) return;
478
+ mapMethodsRef.current.resetAllProvinceColors();
479
+ };
480
+
481
+ // Handle random colors
482
+ const handleRandomColors = () => {
483
+ if (!mapMethodsRef.current) return;
484
+ mapMethodsRef.current.applyRandomColors();
485
+ };
486
+
487
+ // Handle color change from color picker
488
+ const handleColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
489
+ setColorHex(e.target.value);
490
+ };
491
+
492
+ // Handle manual hex input
493
+ const handleHexInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
494
+ const value = e.target.value;
495
+ // Only update if it's a valid hex color or empty
496
+ if (/^#([0-9A-F]{3}){1,2}$/i.test(value) || value === '' || value === '#') {
497
+ setColorHex(value);
498
+ }
499
+ };
500
+
501
+ // Handle coloring all provinces
502
+ const handleColorAllProvinces = () => {
503
+ if (!mapMethodsRef.current) return;
504
+ mapMethodsRef.current.fillAllProvinces(colorHex);
505
+ };
506
+
507
+ // Handle adding a marker at specific coordinates
508
+ const handleAddMarker = () => {
509
+ if (!mapMethodsRef.current) return;
510
+
511
+ mapMethodsRef.current.addMarker({
512
+ id: markerId,
513
+ position: { x: markerX, y: markerY },
514
+ customMarker: {
515
+ svg: markerSvg,
516
+ style: {
517
+ width: `${markerWidth}px`,
518
+ height: `${markerHeight}px`,
519
+ }
520
+ },
521
+ onClick: () => alert(`Marker ${markerId} clicked!`)
522
+ });
523
+
524
+ // Add to markers list if not already there
525
+ if (!markers.includes(markerId)) {
526
+ setMarkers([...markers, markerId]);
527
+ }
528
+ };
529
+
530
+ const handleAddCustomMarker = () => {
531
+ if (!mapMethodsRef.current) return;
532
+
533
+ mapMethodsRef.current.addFixedRatioMarker({
534
+ id: markerId,
535
+ position: { x: markerCustomX, y: markerCustomY },
536
+ customMarker: {
537
+ svg: markerSvg,
538
+ style: {
539
+ width: `${markerWidth}px`,
540
+ height: `${markerHeight}px`,
541
+ }
542
+ },
543
+ onClick: () => alert(`Marker ${markerId} clicked!`)
544
+ });
545
+
546
+ // Add to markers list if not already there
547
+ if (!markers.includes(markerId)) {
548
+ setMarkers([...markers, markerId]);
549
+ }
550
+ };
551
+
552
+
553
+ // Handle adding a marker to a province
554
+ const handleAddMarkerToProvince = () => {
555
+ if (!mapMethodsRef.current || !selectedProvinceForMarker) return;
556
+
557
+ mapMethodsRef.current.addMarkerToProvince(
558
+ selectedProvinceForMarker,
559
+ {
560
+ id: `province-${selectedProvinceForMarker}`,
561
+ svg: `province-${selectedProvinceForMarker}`,
562
+ customMarker: {
563
+ svg: markerSvg,
564
+ style: {
565
+ width: `${markerWidth}px`,
566
+ height: `${markerHeight}px`,
567
+ }
568
+ }
569
+ },
570
+ );
571
+
572
+ // Add to markers list
573
+ const newMarkerId = `province-${selectedProvinceForMarker}`;
574
+ if (!markers.includes(newMarkerId)) {
575
+ setMarkers([...markers, newMarkerId]);
576
+ }
577
+ };
578
+
579
+ // Handle removing a specific marker
580
+ const handleRemoveMarker = (id: string) => {
581
+ if (!mapMethodsRef.current) return;
582
+
583
+ mapMethodsRef.current.removeMarker(id);
584
+ setMarkers(markers.filter(m => m !== id));
585
+ };
586
+
587
+ // Handle removing all markers
588
+ const handleRemoveAllMarkers = () => {
589
+ if (!mapMethodsRef.current) return;
590
+
591
+ mapMethodsRef.current.removeAllMarkers();
592
+ setMarkers([]);
593
+ };
594
+
595
+ // Handle toggling province labels
596
+ const handleToggleProvinceLabels = () => {
597
+ if (!mapMethodsRef.current) return;
598
+
599
+ // Calculate the background color with opacity
600
+ const bgColorWithOpacity = `${labelBgColor}${Math.round(labelBgOpacity * 2.55).toString(16).padStart(2, '0')}`;
601
+
602
+ mapMethodsRef.current.toggleProvinceLabels({
603
+ fontSize: labelFontSize,
604
+ color: labelColor,
605
+ backgroundColor: bgColorWithOpacity
606
+ });
607
+ };
608
+
609
+ // Clean up when component unmounts or map is reset
610
+ useEffect(() => {
611
+ return () => {
612
+ if (cleanup) {
613
+ cleanup();
614
+ }
615
+ };
616
+ }, [cleanup, mapKey]);
617
+
618
+ /**
619
+ * Converts an SVG element to a PNG image element with transparent background
620
+ * @param svgElement The SVG element to convert
621
+ * @param scale Optional scale factor for higher resolution output
622
+ * @returns Promise that resolves with an HTMLImageElement
623
+ */
624
+ const convertSvgToPngElement = async (
625
+ svgElement: SVGElement,
626
+ scale: number = 2,
627
+ offsetX: number = 55,
628
+ ): Promise<HTMLImageElement> => {
629
+ return new Promise((resolve, reject) => {
630
+ try {
631
+ // debugger
632
+ // Get SVG data
633
+ const svgData = new XMLSerializer().serializeToString(svgElement);
634
+
635
+ // Create base64 encoded version for the Image object
636
+ const svgBase64 = btoa(unescape(encodeURIComponent(svgData)));
637
+ const dataUrl = `data:image/svg+xml;base64,${svgBase64}`;
638
+
639
+ // Get dimensions
640
+ const svgRect = svgElement.getBoundingClientRect();
641
+
642
+ // Create canvas with appropriate dimensions
643
+ const canvas = document.createElement('canvas');
644
+ canvas.width = svgRect.width * scale;
645
+ canvas.height = svgRect.height * scale;
646
+
647
+ // Get canvas context and configure it
648
+ const ctx = canvas.getContext('2d');
649
+ if (!ctx) {
650
+ throw new Error('Could not get canvas context');
651
+ }
652
+
653
+ // Clear the canvas to ensure transparency
654
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
655
+
656
+ // Scale the context for higher resolution
657
+ ctx.scale(scale, scale);
658
+
659
+ // Create image from SVG
660
+ const tempImg = new Image();
661
+ tempImg.onload = () => {
662
+
663
+ const scale = svgRect.height / tempImg.height;
664
+ const toDrawImageWidth = tempImg.width * scale;
665
+ const toDrawImageHeight = tempImg.height * scale;
666
+
667
+ // const toDrawImageWidth = tempImg.width;
668
+ // const toDrawImageHeight = tempImg.height;
669
+
670
+ // Draw the image to the canvas
671
+ ctx.drawImage(tempImg, 0, 0, toDrawImageWidth, toDrawImageHeight);
672
+
673
+ // Convert canvas to PNG data URL
674
+ const pngDataUrl = canvas.toDataURL('image/png');
675
+
676
+ setSvgImage(tempImg.src);
677
+ setSvg2PngImage(pngDataUrl);
678
+
679
+ // Create the final image element with the PNG data
680
+ const imgElement = new Image();
681
+
682
+ // Set styles for full width and height
683
+ imgElement.style.width = '100%';
684
+ imgElement.style.height = '100%';
685
+
686
+ // Set the style attribute for HTML serialization
687
+ imgElement.setAttribute('style', imgElement.style.cssText);
688
+
689
+ // Set the source to the PNG data URL
690
+ imgElement.onload = () => resolve(imgElement);
691
+ imgElement.onerror = (error) => reject(error);
692
+ imgElement.src = pngDataUrl;
693
+ };
694
+
695
+ tempImg.onerror = (error) => {
696
+ reject(new Error(`Failed to load SVG as image: ${error}`));
697
+ };
698
+
699
+ // Set the source to trigger loading
700
+ tempImg.src = dataUrl;
701
+ } catch (error) {
702
+ reject(error);
703
+ }
704
+ });
705
+ };
706
+
707
+ // /**
708
+ // * Converts an SVG element to a PNG image element with transparent background using canvg
709
+ // * @param svgElement The SVG element to convert
710
+ // * @param scale Optional scale factor for higher resolution output
711
+ // * @returns Promise that resolves with an HTMLImageElement
712
+ // */
713
+ // const convertSvgToPngElementWithCanvg = async (
714
+ // svgElement: SVGElement,
715
+ // scale: number = 1
716
+ // ): Promise<HTMLImageElement> => {
717
+ // return new Promise(async (resolve, reject) => {
718
+ // try {
719
+ // // Dynamically import canvg to avoid SSR issues
720
+ // const { Canvg } = await import('canvg');
721
+
722
+ // // Get SVG data
723
+ // const svgData = new XMLSerializer().serializeToString(svgElement);
724
+
725
+ // // Get dimensions
726
+ // const svgRect = svgElement.getBoundingClientRect();
727
+ // const width = svgRect.width;
728
+ // const height = svgRect.height;
729
+
730
+ // // Create canvas with appropriate dimensions
731
+ // const canvas = document.createElement('canvas');
732
+ // canvas.width = width * scale;
733
+ // canvas.height = height * scale;
734
+
735
+ // // Get canvas context
736
+ // const ctx = canvas.getContext('2d');
737
+ // if (!ctx) {
738
+ // throw new Error('Could not get canvas context');
739
+ // }
740
+
741
+ // // Clear the canvas to ensure transparency
742
+ // ctx.clearRect(0, 0, canvas.width, canvas.height);
743
+
744
+ // // Create canvg instance with proper configuration
745
+ // const v = await Canvg.fromString(ctx, svgData, {
746
+ // ignoreMouse: true,
747
+ // ignoreAnimation: true,
748
+ // enableRedraw: false,
749
+ // ignoreDimensions: true, // Important for proper sizing
750
+ // scaleWidth: width * scale,
751
+ // scaleHeight: height * scale,
752
+ // offsetX: 0,
753
+ // offsetY: 0,
754
+ // forceRedraw: () => false
755
+ // });
756
+
757
+ // // Render the SVG to canvas
758
+ // await v.render();
759
+
760
+ // // Convert canvas to PNG data URL
761
+ // const pngDataUrl = canvas.toDataURL('image/png');
762
+
763
+ // // Create the final image element with the PNG data
764
+ // const imgElement = new Image();
765
+
766
+ // // Set styles for full width and height
767
+ // imgElement.style.width = '100%';
768
+ // imgElement.style.height = '100%';
769
+ // imgElement.style.position = 'absolute';
770
+ // imgElement.style.top = '0';
771
+ // imgElement.style.left = '0';
772
+
773
+ // // Set the style attribute for HTML serialization
774
+ // imgElement.setAttribute('style', imgElement.style.cssText);
775
+
776
+ // // Set the source to the PNG data URL
777
+ // imgElement.onload = () => resolve(imgElement);
778
+ // imgElement.onerror = (error) => reject(error);
779
+ // imgElement.src = pngDataUrl;
780
+
781
+ // } catch (error) {
782
+ // console.error('Error in convertSvgToPngElement:', error);
783
+ // reject(error);
784
+ // }
785
+ // });
786
+ // };
787
+
788
+ // Handle converting map to image
789
+ const handleConvertToImage = async () => {
790
+ if (!mapCreated || !mapContainerRef.current) return;
791
+
792
+ setIsGeneratingImage(true);
793
+
794
+ try {
795
+ // clone map container
796
+ const clonedMapContainer = mapContainerRef.current.cloneNode(true) as HTMLElement;
797
+
798
+ // Find all SVG elements in the map container
799
+ const svgElements = clonedMapContainer.querySelectorAll('svg');
800
+ console.log("Found SVG elements:", svgElements.length);
801
+
802
+ svgElements.forEach(svg => {
803
+ console.log("SVG element:", svg);
804
+ });
805
+
806
+ if (svgElements.length === 0) {
807
+ throw new Error('No SVG elements found in the map');
808
+ }
809
+
810
+ // Replace all SVGs with image elements
811
+ const svgPromises = Array.from(svgElements).map(async (svg) => {
812
+ try {
813
+ // Convert SVG to image element
814
+ const imgElement = await convertSvgToPngElement(svg as SVGElement, 2);
815
+
816
+ // Replace the SVG with the image
817
+ if (svg.parentNode) {
818
+ svg.parentNode.replaceChild(imgElement, svg);
819
+ }
820
+ } catch (error) {
821
+ console.error('Failed to convert SVG to image:', error);
822
+ }
823
+ });
824
+
825
+ // Wait for all replacements to complete
826
+ await Promise.all(svgPromises);
827
+
828
+ // Add a small delay to ensure DOM updates are complete
829
+ await new Promise(resolve => setTimeout(resolve, 100));
830
+
831
+ // Create a template component with the modified map container
832
+ const templateDiv = (
833
+ <div
834
+ style={{
835
+ width: mapContainerRef.current.clientWidth,
836
+ height: mapContainerRef.current.clientHeight,
837
+ }}
838
+ dangerouslySetInnerHTML={{ __html: clonedMapContainer.innerHTML }}
839
+ />
840
+ );
841
+
842
+ setTemplateComponent(templateDiv);
843
+
844
+ // Generate canvas from the template
845
+ const canvas = await generateCanvas({
846
+ templateComponent: templateDiv,
847
+ options: {
848
+ scale: 2
849
+ }
850
+ });
851
+
852
+ if (canvas) {
853
+ // Convert canvas to data URL
854
+ const dataUrl = canvas.toDataURL('image/png');
855
+ setPreviewImage(dataUrl);
856
+ }
857
+
858
+ // Restore the original content
859
+ // mapContainerRef.current.innerHTML = originalContent;
860
+
861
+ } catch (error) {
862
+ console.error('Failed to convert map to image:', error);
863
+ alert('Failed to convert map to image. Please try again.');
864
+ } finally {
865
+ setIsGeneratingImage(false);
866
+ }
867
+ };
868
+
869
+ return (
870
+ <Container>
871
+ <MapContainer ref={mapContainerRef}>
872
+ {mapCreated ? mapInstance : (
873
+ <div style={{
874
+ display: 'flex',
875
+ justifyContent: 'center',
876
+ alignItems: 'center',
877
+ height: '100%',
878
+ color: '#666',
879
+ padding: '16px',
880
+ textAlign: 'center'
881
+ }}>
882
+ {isMobile
883
+ ? 'Configure and create the map below'
884
+ : 'Configure and create the map using the panel on the right'}
885
+ </div>
886
+ )}
887
+ </MapContainer>
888
+
889
+ <DebugPanel>
890
+ <DebugPanelContent>
891
+ <SectionTitle>Map Configuration</SectionTitle>
892
+
893
+ <FormGroup>
894
+ <Label>Container Width</Label>
895
+ <Input
896
+ type="text"
897
+ value={containerWidth}
898
+ onChange={(e) => setContainerWidth(e.target.value)}
899
+ placeholder="e.g., 600px or 100%"
900
+ disabled={mapCreated}
901
+ />
902
+ </FormGroup>
903
+
904
+ <FormGroup>
905
+ <Label>Container Height</Label>
906
+ <Input
907
+ type="text"
908
+ value={containerHeight}
909
+ onChange={(e) => setContainerHeight(e.target.value)}
910
+ placeholder="e.g., 500px or 100%"
911
+ disabled={mapCreated}
912
+ />
913
+ </FormGroup>
914
+
915
+ {!mapCreated ? (
916
+ <Button onClick={handleCreateMap}>
917
+ Create Map
918
+ </Button>
919
+ ) : (
920
+ <Button
921
+ onClick={handleResetMap}
922
+ style={{ backgroundColor: '#FF9800' }}
923
+ >
924
+ Reset & Create New Map
925
+ </Button>
926
+ )}
927
+
928
+ <Divider />
929
+
930
+ <SectionTitle>Province Coloring</SectionTitle>
931
+
932
+ <FormGroup>
933
+ <Label>Select Province</Label>
934
+ <Select
935
+ value={selectedProvince}
936
+ onChange={(e) => setSelectedProvince(e.target.value)}
937
+ disabled={!mapCreated}
938
+ >
939
+ <option value="">-- Select Province --</option>
940
+ {vietnamProvinces.map(province => (
941
+ <option key={province.id} value={province.id}>
942
+ {province.name}
943
+ </option>
944
+ ))}
945
+ </Select>
946
+ </FormGroup>
947
+
948
+ <FormGroup>
949
+ <Label>Select Region</Label>
950
+ <Select
951
+ value={selectedRegion}
952
+ onChange={(e) => setSelectedRegion(e.target.value)}
953
+ disabled={!mapCreated}
954
+ >
955
+ <option value="">-- Select Region --</option>
956
+ {vietnamRegions.map(region => (
957
+ <option key={region.id} value={region.id}>
958
+ {region.name}
959
+ </option>
960
+ ))}
961
+ </Select>
962
+ </FormGroup>
963
+
964
+ <FormGroup>
965
+ <Label>Color</Label>
966
+ <ColorInputContainer>
967
+ <ColorPickerWrapper>
968
+ <ColorInput
969
+ type="color"
970
+ value={colorHex}
971
+ onChange={handleColorChange}
972
+ disabled={!mapCreated}
973
+ />
974
+ </ColorPickerWrapper>
975
+ <HexInput
976
+ type="text"
977
+ value={colorHex}
978
+ onChange={handleHexInputChange}
979
+ placeholder="#RRGGBB"
980
+ disabled={!mapCreated}
981
+ maxLength={7}
982
+ />
983
+ </ColorInputContainer>
984
+ </FormGroup>
985
+
986
+ <ButtonGroup>
987
+ <Button
988
+ onClick={handleColorProvince}
989
+ disabled={!mapCreated || !selectedProvince}
990
+ >
991
+ Color Province
992
+ </Button>
993
+
994
+ <Button
995
+ onClick={handleColorRegion}
996
+ disabled={!mapCreated || !selectedRegion}
997
+ >
998
+ Color Region
999
+ </Button>
1000
+ </ButtonGroup>
1001
+
1002
+ <Button
1003
+ onClick={handleColorAllProvinces}
1004
+ disabled={!mapCreated}
1005
+ style={{ backgroundColor: '#9C27B0' }}
1006
+ >
1007
+ Color All Provinces
1008
+ </Button>
1009
+
1010
+ <Divider />
1011
+
1012
+ <ButtonGroup>
1013
+ <Button
1014
+ onClick={handleResetColors}
1015
+ disabled={!mapCreated}
1016
+ style={{ backgroundColor: '#f44336' }}
1017
+ >
1018
+ Reset All Colors
1019
+ </Button>
1020
+
1021
+ <Button
1022
+ onClick={handleRandomColors}
1023
+ disabled={!mapCreated}
1024
+ style={{ backgroundColor: '#4CAF50' }}
1025
+ >
1026
+ Random Colors
1027
+ </Button>
1028
+ </ButtonGroup>
1029
+
1030
+ <Divider />
1031
+
1032
+ <SectionTitle>Markers</SectionTitle>
1033
+
1034
+ <MarkerSection>
1035
+ <FormGroup>
1036
+ <Label>Marker ID</Label>
1037
+ <Input
1038
+ type="text"
1039
+ value={markerId}
1040
+ onChange={(e) => setMarkerId(e.target.value)}
1041
+ placeholder="Enter marker ID"
1042
+ disabled={!mapCreated}
1043
+ />
1044
+ </FormGroup>
1045
+
1046
+ <FormGroup>
1047
+ <Label>Marker SVG</Label>
1048
+ <textarea rows={5} name="markerSvg" id="markerSvg" value={markerSvg} onChange={(e) => setMarkerSvg(e.target.value)}></textarea>
1049
+ </FormGroup>
1050
+
1051
+ <FormGroup>
1052
+ <Label>Marker Size</Label>
1053
+ <CoordinateInputs>
1054
+ <CoordinateInput
1055
+ type="number"
1056
+ value={markerWidth}
1057
+ onChange={(e) => setMarkerWidth(Number(e.target.value))}
1058
+ placeholder="10px"
1059
+ disabled={!mapCreated}
1060
+ />
1061
+ <CoordinateInput
1062
+ type="number"
1063
+ value={markerHeight}
1064
+ onChange={(e) => setMarkerHeight(Number(e.target.value))}
1065
+ placeholder="10px"
1066
+ disabled={!mapCreated}
1067
+ />
1068
+ </CoordinateInputs>
1069
+ </FormGroup>
1070
+
1071
+ <FormGroup>
1072
+ <Label>Marker Coordinates</Label>
1073
+ <CoordinateInputs>
1074
+ <CoordinateInput
1075
+ type="number"
1076
+ value={markerX}
1077
+ onChange={(e) => setMarkerX(Number(e.target.value))}
1078
+ placeholder="X"
1079
+ disabled={!mapCreated}
1080
+ />
1081
+ <CoordinateInput
1082
+ type="number"
1083
+ value={markerY}
1084
+ onChange={(e) => setMarkerY(Number(e.target.value))}
1085
+ placeholder="Y"
1086
+ disabled={!mapCreated}
1087
+ />
1088
+ </CoordinateInputs>
1089
+ </FormGroup>
1090
+
1091
+ <Button
1092
+ onClick={handleAddMarker}
1093
+ disabled={!mapCreated}
1094
+ style={{ backgroundColor: '#2196F3' }}
1095
+ >
1096
+ Add Marker at Coordinates
1097
+ </Button>
1098
+
1099
+ <FormGroup>
1100
+ <Label>Custom Marker Coordinates</Label>
1101
+ <CoordinateInputs>
1102
+ <CoordinateInput
1103
+ type="number"
1104
+ value={markerCustomX}
1105
+ onChange={(e) => setMarkerCustomX(Number(e.target.value))}
1106
+ placeholder="X"
1107
+ disabled={!mapCreated}
1108
+ />
1109
+ <CoordinateInput
1110
+ type="number"
1111
+ value={markerCustomY}
1112
+ onChange={(e) => setMarkerCustomY(Number(e.target.value))}
1113
+ placeholder="Y"
1114
+ disabled={!mapCreated}
1115
+ />
1116
+ </CoordinateInputs>
1117
+ </FormGroup>
1118
+
1119
+ <Button
1120
+ onClick={handleAddCustomMarker}
1121
+ disabled={!mapCreated}
1122
+ style={{ backgroundColor: '#2196F3' }}
1123
+ >
1124
+ Add Custom Marker at Coordinates
1125
+ </Button>
1126
+
1127
+ <FormGroup>
1128
+ <Label>Add Marker to Province</Label>
1129
+ <Select
1130
+ value={selectedProvinceForMarker}
1131
+ onChange={(e) => setSelectedProvinceForMarker(e.target.value)}
1132
+ disabled={!mapCreated}
1133
+ >
1134
+ <option value="">-- Select Province --</option>
1135
+ {vietnamProvinces.map(province => (
1136
+ <option key={province.id} value={province.id}>
1137
+ {province.name}
1138
+ </option>
1139
+ ))}
1140
+ </Select>
1141
+ </FormGroup>
1142
+
1143
+ <Button
1144
+ onClick={handleAddMarkerToProvince}
1145
+ disabled={!mapCreated || !selectedProvinceForMarker}
1146
+ style={{ backgroundColor: '#2196F3' }}
1147
+ >
1148
+ Add Marker to Province
1149
+ </Button>
1150
+
1151
+ {markers.length > 0 && (
1152
+ <>
1153
+ <Label>Active Markers</Label>
1154
+ <div style={{ maxHeight: '150px', overflowY: 'auto', border: '1px solid #ccc', borderRadius: '4px', padding: '8px' }}>
1155
+ {markers.map(id => (
1156
+ <div key={id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
1157
+ <span>{id}</span>
1158
+ <button
1159
+ onClick={() => handleRemoveMarker(id)}
1160
+ style={{
1161
+ background: '#f44336',
1162
+ color: 'white',
1163
+ border: 'none',
1164
+ borderRadius: '4px',
1165
+ padding: '4px 8px',
1166
+ cursor: 'pointer'
1167
+ }}
1168
+ >
1169
+ Remove
1170
+ </button>
1171
+ </div>
1172
+ ))}
1173
+ </div>
1174
+
1175
+ <Button
1176
+ onClick={handleRemoveAllMarkers}
1177
+ disabled={!mapCreated || markers.length === 0}
1178
+ style={{ backgroundColor: '#f44336' }}
1179
+ >
1180
+ Remove All Markers
1181
+ </Button>
1182
+ </>
1183
+ )}
1184
+ </MarkerSection>
1185
+
1186
+ <Divider />
1187
+
1188
+ <SectionTitle>Province Labels</SectionTitle>
1189
+
1190
+ <LabelSection>
1191
+ <FormGroup>
1192
+ <Label>Label Settings</Label>
1193
+ <LabelOptions>
1194
+ <FormGroup>
1195
+ <Label>Font Size</Label>
1196
+ <Input
1197
+ type="number"
1198
+ value={labelFontSize}
1199
+ onChange={(e) => setLabelFontSize(Number(e.target.value))}
1200
+ min={6}
1201
+ max={24}
1202
+ disabled={!mapCreated}
1203
+ />
1204
+ </FormGroup>
1205
+ <FormGroup>
1206
+ <Label>Background Opacity</Label>
1207
+ <Input
1208
+ type="range"
1209
+ value={labelBgOpacity}
1210
+ onChange={(e) => setLabelBgOpacity(Number(e.target.value))}
1211
+ min={0}
1212
+ max={100}
1213
+ disabled={!mapCreated}
1214
+ />
1215
+ </FormGroup>
1216
+ </LabelOptions>
1217
+
1218
+ <LabelOptions>
1219
+ <FormGroup>
1220
+ <Label>Text Color</Label>
1221
+ <ColorInputContainer>
1222
+ <ColorInput
1223
+ type="color"
1224
+ value={labelColor}
1225
+ onChange={(e) => setLabelColor(e.target.value)}
1226
+ disabled={!mapCreated}
1227
+ />
1228
+ </ColorInputContainer>
1229
+ </FormGroup>
1230
+ <FormGroup>
1231
+ <Label>Background Color</Label>
1232
+ <ColorInputContainer>
1233
+ <ColorInput
1234
+ type="color"
1235
+ value={labelBgColor}
1236
+ onChange={(e) => setLabelBgColor(e.target.value)}
1237
+ disabled={!mapCreated}
1238
+ />
1239
+ </ColorInputContainer>
1240
+ </FormGroup>
1241
+ </LabelOptions>
1242
+ </FormGroup>
1243
+
1244
+ <Button
1245
+ onClick={handleToggleProvinceLabels}
1246
+ disabled={!mapCreated}
1247
+ style={{ backgroundColor: isProvinceLabelsVisible ? '#f44336' : '#4CAF50' }}
1248
+ >
1249
+ {isProvinceLabelsVisible ? 'Hide Province Names' : 'Show Province Names'}
1250
+ </Button>
1251
+ </LabelSection>
1252
+
1253
+ <Divider />
1254
+
1255
+ <SectionTitle>Export Map</SectionTitle>
1256
+
1257
+ <Button
1258
+ onClick={handleConvertToImage}
1259
+ disabled={!mapCreated || isGeneratingImage}
1260
+ style={{ backgroundColor: '#795548' }}
1261
+ >
1262
+ {isGeneratingImage ? 'Generating Image...' : 'Convert to Image'}
1263
+ </Button>
1264
+
1265
+ {
1266
+ svgImage && (
1267
+ <>
1268
+ <Label>Temp</Label>
1269
+ <PreviewImage>
1270
+ <img src={svgImage} alt="Image Preview" />
1271
+ </PreviewImage>
1272
+ </>
1273
+ )
1274
+ }
1275
+
1276
+ {
1277
+ svg2PngImage && (
1278
+ <>
1279
+ <Label>SVG2PNG</Label>
1280
+ <PreviewImage>
1281
+ <img src={svg2PngImage} alt="Image Preview" />
1282
+ </PreviewImage>
1283
+ </>
1284
+ )
1285
+ }
1286
+
1287
+ {previewImage && (
1288
+ <PreviewImage>
1289
+ <img src={previewImage} alt="Map Preview" />
1290
+ </PreviewImage>
1291
+ )}
1292
+
1293
+ {
1294
+ !!templateComponent && (
1295
+ <>
1296
+ <Label>Template Component</Label>
1297
+ {templateComponent}
1298
+ </>
1299
+ )
1300
+ }
1301
+
1302
+ {previewImage && (
1303
+ <Button
1304
+ onClick={() => {
1305
+ // Create a temporary link to download the image
1306
+ const link = document.createElement('a');
1307
+ link.href = previewImage;
1308
+ link.download = 'vietnam-map.png';
1309
+ document.body.appendChild(link);
1310
+ link.click();
1311
+ document.body.removeChild(link);
1312
+ }}
1313
+ style={{ backgroundColor: '#607D8B', marginTop: '8px' }}
1314
+ >
1315
+ Download Image
1316
+ </Button>
1317
+ )}
1318
+
1319
+ </DebugPanelContent>
1320
+ </DebugPanel>
1321
+ </Container>
1322
+ );
1323
+ };
1324
+
1325
+ export default MapView;