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.
- package/example/README.md +50 -0
- package/example/eslint.config.js +28 -0
- package/example/index.html +13 -0
- package/example/package.json +35 -0
- package/example/public/vite.svg +1 -0
- package/example/src/App.css +8 -0
- package/example/src/App.tsx +62 -0
- package/example/src/Card/index.tsx +15 -0
- package/example/src/Icons/CircleArrow.tsx +8 -0
- package/example/src/Icons/icon1.tsx +19 -0
- package/example/src/View/CoreComponentView/index.tsx +255 -0
- package/example/src/View/MapView/index.tsx +1325 -0
- package/example/src/View/MapView/map.html +14 -0
- package/example/src/assets/fonts/Kanit/Kanit-Black.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-BlackItalic.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-Bold.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-BoldItalic.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-ExtraBold.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-ExtraBoldItalic.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-ExtraLight.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-ExtraLightItalic.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-Italic.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-Light.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-LightItalic.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-Medium.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-MediumItalic.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-Regular.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-SemiBold.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-SemiBoldItalic.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-Thin.ttf +0 -0
- package/example/src/assets/fonts/Kanit/Kanit-ThinItalic.ttf +0 -0
- package/example/src/assets/fonts/Kanit/OFL.txt +93 -0
- package/example/src/assets/fonts/Roboto/Roboto-Black.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-BlackItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-Bold.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-BoldItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-ExtraBold.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-ExtraBoldItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-ExtraLight.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-ExtraLightItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-Italic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-Light.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-LightItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-Medium.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-MediumItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-Regular.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-SemiBold.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-SemiBoldItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-Thin.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto-ThinItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-Black.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-BlackItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-Bold.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-BoldItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-ExtraBold.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-ExtraBoldItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-ExtraLight.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-ExtraLightItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-Italic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-Light.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-LightItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-Medium.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-MediumItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-Regular.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-SemiBold.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-SemiBoldItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-Thin.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_Condensed-ThinItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-Black.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-BlackItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-Bold.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-BoldItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-ExtraBold.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-ExtraBoldItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-ExtraLight.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-ExtraLightItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-Italic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-Light.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-LightItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-Medium.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-MediumItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-Regular.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-SemiBold.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-SemiBoldItalic.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-Thin.ttf +0 -0
- package/example/src/assets/fonts/Roboto/Roboto_SemiCondensed-ThinItalic.ttf +0 -0
- package/example/src/assets/fonts/iCielBCLodestone/iCielBCLodestone.ttf +0 -0
- package/example/src/assets/react.svg +1 -0
- package/example/src/fonts.css +66 -0
- package/example/src/index.css +70 -0
- package/example/src/main.tsx +10 -0
- package/example/src/vite-env.d.ts +1 -0
- package/example/tsconfig.app.json +26 -0
- package/example/tsconfig.json +7 -0
- package/example/tsconfig.node.json +24 -0
- package/example/vite.config.ts +7 -0
- package/index.css +20 -0
- package/index.ts +42 -0
- package/package.json +44 -0
- package/src/components/AppbarPrimaryButton/index.tsx +56 -0
- package/src/components/Avatar.styled/index.tsx +8 -0
- package/src/components/Button.styled/index.tsx +154 -0
- package/src/components/Card.styled/index.tsx +26 -0
- package/src/components/Compound/Appbar/index.tsx +88 -0
- package/src/components/Compound/Header/index.tsx +40 -0
- package/src/components/Compound/InteractiveMap/MapSvg.tsx +608 -0
- package/src/components/Compound/LocationOverviewCard/index.tsx +186 -0
- package/src/components/Compound/MemoryCard/index.tsx +267 -0
- package/src/components/Compound/ProgressStep/index.tsx +54 -0
- package/src/components/Compound/SponsorByContainer/index.tsx +31 -0
- package/src/components/FormItem.styled/index.tsx +23 -0
- package/src/components/Icons/AppbarBg.tsx +22 -0
- package/src/components/Icons/ArrowCircle.tsx +8 -0
- package/src/components/Icons/ArrowDown.tsx +15 -0
- package/src/components/Icons/Camera.tsx +17 -0
- package/src/components/Icons/Check.tsx +7 -0
- package/src/components/Icons/ChevronLeft.tsx +7 -0
- package/src/components/Icons/ChevronRight.tsx +16 -0
- package/src/components/Icons/CircleAlert.tsx +9 -0
- package/src/components/Icons/CircleArrow.tsx +8 -0
- package/src/components/Icons/CircleCheck.tsx +7 -0
- package/src/components/Icons/CornerUpRight.tsx +7 -0
- package/src/components/Icons/Dart.tsx +7 -0
- package/src/components/Icons/Discover.tsx +16 -0
- package/src/components/Icons/Edit.tsx +16 -0
- package/src/components/Icons/Email.tsx +16 -0
- package/src/components/Icons/Exclaimation.tsx +7 -0
- package/src/components/Icons/Facebook.tsx +7 -0
- package/src/components/Icons/Gear.tsx +15 -0
- package/src/components/Icons/Gift.tsx +18 -0
- package/src/components/Icons/Globe.tsx +14 -0
- package/src/components/Icons/Home.tsx +7 -0
- package/src/components/Icons/Icon1.tsx +19 -0
- package/src/components/Icons/Icon1sm.tsx +19 -0
- package/src/components/Icons/Instagram.tsx +9 -0
- package/src/components/Icons/Link.tsx +16 -0
- package/src/components/Icons/LocationPin.tsx +10 -0
- package/src/components/Icons/Logout.tsx +15 -0
- package/src/components/Icons/MapMarker.tsx +8 -0
- package/src/components/Icons/Marker/MarkerRed.tsx +17 -0
- package/src/components/Icons/Menu.tsx +11 -0
- package/src/components/Icons/Mission.tsx +17 -0
- package/src/components/Icons/Moment.tsx +18 -0
- package/src/components/Icons/Phone.tsx +15 -0
- package/src/components/Icons/Pin.tsx +8 -0
- package/src/components/Icons/PinCircle.tsx +17 -0
- package/src/components/Icons/PinOutlined.tsx +7 -0
- package/src/components/Icons/Profile.tsx +15 -0
- package/src/components/Icons/ProfileGift.tsx +23 -0
- package/src/components/Icons/ProgressBar.tsx +20 -0
- package/src/components/Icons/ProgressBarInner.tsx +44 -0
- package/src/components/Icons/SealCheckIcon.tsx +18 -0
- package/src/components/Icons/Search.tsx +7 -0
- package/src/components/Icons/SendMessage.tsx +7 -0
- package/src/components/Icons/Share.tsx +14 -0
- package/src/components/Icons/ShareMemoryBadge.tsx +11 -0
- package/src/components/Icons/ShieldWarningIcon.tsx +18 -0
- package/src/components/Icons/Spinner.tsx +18 -0
- package/src/components/Icons/Step.tsx +7 -0
- package/src/components/Icons/StepChecked.tsx +8 -0
- package/src/components/Icons/StepLine.tsx +7 -0
- package/src/components/Icons/Telegram.tsx +7 -0
- package/src/components/Icons/Trash.tsx +7 -0
- package/src/components/Icons/User.tsx +15 -0
- package/src/components/Icons/XIcon.tsx +8 -0
- package/src/components/Icons/Zalo.tsx +23 -0
- package/src/components/Icons/index.tsx +66 -0
- package/src/components/Image.styled/index.tsx +35 -0
- package/src/components/Input.styled/index.tsx +34 -0
- package/src/components/InputPassword/index.tsx +34 -0
- package/src/components/InputSearch.styled/index.tsx +18 -0
- package/src/components/Loader.styled/index.tsx +26 -0
- package/src/components/Modal.styled/index.tsx +63 -0
- package/src/components/ProcessBar.styled/index.tsx +26 -0
- package/src/components/ProgressCircular.styled/index.tsx +61 -0
- package/src/components/SVGs/NoResult.tsx +62 -0
- package/src/components/SVGs/index.tsx +3 -0
- package/src/components/Select.styled/index.tsx +32 -0
- package/src/components/Tabs.styled/index.tsx +22 -0
- package/src/components/TextArea.styled/index.tsx +34 -0
- package/src/components/Typography.styled/index.tsx +419 -0
- package/src/helpers/index.ts +175 -0
- package/src/hooks/useHTMLToCanvas.tsx +115 -0
- package/src/hooks/useInteractiveMap.tsx +659 -0
- package/src/types/type.d.ts +9 -0
- package/tsconfig.json +33 -0
- package/tsconfig.node.json +12 -0
- 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;
|