@salesforcedevs/docs-components 1.17.0-edit → 1.17.0-hack-alpha
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/lwc.config.json +1 -1
- package/package.json +1 -1
- package/src/modules/doc/commentPopup/commentPopup.css +332 -0
- package/src/modules/doc/commentPopup/commentPopup.html +119 -0
- package/src/modules/doc/commentPopup/commentPopup.ts +213 -0
- package/src/modules/doc/contentLayout/contentLayout.html +1 -0
- package/src/modules/doc/lwcContentLayout/lwcContentLayout.html +1 -0
- package/src/modules/docHelpers/contentLayoutStyle/contentLayoutStyle.css +1 -0
- package/src/modules/doc/markdownEditor/markdownEditor.css +0 -166
- package/src/modules/doc/markdownEditor/markdownEditor.html +0 -68
- package/src/modules/doc/markdownEditor/markdownEditor.ts +0 -118
package/lwc.config.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"expose": [
|
|
8
8
|
"doc/amfReference",
|
|
9
9
|
"doc/breadcrumbs",
|
|
10
|
+
"doc/commentPopup",
|
|
10
11
|
"doc/componentPlayground",
|
|
11
12
|
"doc/content",
|
|
12
13
|
"doc/contentCallout",
|
|
@@ -18,7 +19,6 @@
|
|
|
18
19
|
"doc/header",
|
|
19
20
|
"doc/heading",
|
|
20
21
|
"doc/headingAnchor",
|
|
21
|
-
"doc/markdownEditor",
|
|
22
22
|
"doc/overview",
|
|
23
23
|
"doc/phase",
|
|
24
24
|
"doc/specificationContent",
|
package/package.json
CHANGED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/* Comment Icon Container */
|
|
2
|
+
.comment-icon-container {
|
|
3
|
+
position: absolute;
|
|
4
|
+
top: 20px;
|
|
5
|
+
right: 20px;
|
|
6
|
+
z-index: 1000;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.comment-icon-button {
|
|
10
|
+
background: #1976d2;
|
|
11
|
+
border-radius: 50%;
|
|
12
|
+
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
|
|
13
|
+
width: 56px;
|
|
14
|
+
height: 56px;
|
|
15
|
+
border: none;
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
transition: all 0.3s ease;
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.comment-icon-button:hover {
|
|
24
|
+
background: #1565c0;
|
|
25
|
+
transform: scale(1.05);
|
|
26
|
+
box-shadow: 0 6px 16px rgb(0 0 0 / 20%);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.comment-icon-button:active {
|
|
30
|
+
transform: scale(0.95);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Comment Count Badge */
|
|
34
|
+
.comment-count-badge {
|
|
35
|
+
position: absolute;
|
|
36
|
+
top: -8px;
|
|
37
|
+
right: -8px;
|
|
38
|
+
background: #f44336;
|
|
39
|
+
color: white;
|
|
40
|
+
border-radius: 50%;
|
|
41
|
+
width: 20px;
|
|
42
|
+
height: 20px;
|
|
43
|
+
font-size: 12px;
|
|
44
|
+
font-weight: bold;
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
min-width: 20px;
|
|
49
|
+
padding: 0 2px;
|
|
50
|
+
box-sizing: border-box;
|
|
51
|
+
border: 2px solid white;
|
|
52
|
+
box-shadow: 0 2px 4px rgb(0 0 0 / 20%);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Popup Overlay */
|
|
56
|
+
.popup-overlay {
|
|
57
|
+
position: fixed;
|
|
58
|
+
top: 0;
|
|
59
|
+
left: 0;
|
|
60
|
+
width: 100%;
|
|
61
|
+
height: 100%;
|
|
62
|
+
background: rgb(0 0 0 / 50%);
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
justify-content: center;
|
|
66
|
+
z-index: 2000;
|
|
67
|
+
padding: 20px;
|
|
68
|
+
box-sizing: border-box;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Popup Container */
|
|
72
|
+
.popup-container {
|
|
73
|
+
background: white;
|
|
74
|
+
border-radius: 12px;
|
|
75
|
+
box-shadow: 0 20px 40px rgb(0 0 0 / 10%);
|
|
76
|
+
max-width: 500px;
|
|
77
|
+
width: 100%;
|
|
78
|
+
max-height: 80vh;
|
|
79
|
+
overflow-y: auto;
|
|
80
|
+
position: relative;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Popup Header */
|
|
84
|
+
.popup-header {
|
|
85
|
+
display: flex;
|
|
86
|
+
justify-content: space-between;
|
|
87
|
+
align-items: center;
|
|
88
|
+
padding: 24px 24px 0;
|
|
89
|
+
border-bottom: 1px solid #e0e0e0;
|
|
90
|
+
margin-bottom: 24px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.popup-title {
|
|
94
|
+
margin: 0;
|
|
95
|
+
font-size: 20px;
|
|
96
|
+
font-weight: 600;
|
|
97
|
+
color: #212121;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.close-button {
|
|
101
|
+
background: none;
|
|
102
|
+
border: none;
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
padding: 8px;
|
|
105
|
+
border-radius: 50%;
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
justify-content: center;
|
|
109
|
+
transition: background-color 0.2s ease;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.close-button:hover {
|
|
113
|
+
background: #f5f5f5;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Popup Content */
|
|
117
|
+
.popup-content {
|
|
118
|
+
padding: 0 24px 24px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Comment Form */
|
|
122
|
+
.comment-form {
|
|
123
|
+
margin-bottom: 24px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.form-group {
|
|
127
|
+
margin-bottom: 20px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.form-label {
|
|
131
|
+
display: block;
|
|
132
|
+
margin-bottom: 8px;
|
|
133
|
+
font-weight: 500;
|
|
134
|
+
color: #424242;
|
|
135
|
+
font-size: 14px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.form-input,
|
|
139
|
+
.form-textarea {
|
|
140
|
+
width: 100%;
|
|
141
|
+
padding: 12px 16px;
|
|
142
|
+
border: 2px solid #e0e0e0;
|
|
143
|
+
border-radius: 8px;
|
|
144
|
+
font-size: 14px;
|
|
145
|
+
font-family: inherit;
|
|
146
|
+
transition: border-color 0.2s ease;
|
|
147
|
+
box-sizing: border-box;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.form-input:focus,
|
|
151
|
+
.form-textarea:focus {
|
|
152
|
+
outline: none;
|
|
153
|
+
border-color: #1976d2;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.form-textarea {
|
|
157
|
+
resize: vertical;
|
|
158
|
+
min-height: 100px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.error-message {
|
|
162
|
+
color: #d32f2f;
|
|
163
|
+
font-size: 12px;
|
|
164
|
+
margin-top: 4px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.form-actions {
|
|
168
|
+
display: flex;
|
|
169
|
+
justify-content: flex-end;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Comments Section */
|
|
173
|
+
.comments-section {
|
|
174
|
+
border-top: 1px solid #e0e0e0;
|
|
175
|
+
padding-top: 24px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.comments-title {
|
|
179
|
+
margin: 0 0 16px;
|
|
180
|
+
font-size: 16px;
|
|
181
|
+
font-weight: 600;
|
|
182
|
+
color: #212121;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.comments-list {
|
|
186
|
+
display: flex;
|
|
187
|
+
flex-direction: column;
|
|
188
|
+
gap: 16px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.comment-item {
|
|
192
|
+
background: #f8f9fa;
|
|
193
|
+
border-radius: 8px;
|
|
194
|
+
padding: 16px;
|
|
195
|
+
border-left: 4px solid #1976d2;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.comment-header {
|
|
199
|
+
display: flex;
|
|
200
|
+
justify-content: space-between;
|
|
201
|
+
align-items: center;
|
|
202
|
+
margin-bottom: 8px;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.comment-email {
|
|
206
|
+
font-weight: 500;
|
|
207
|
+
color: #1976d2;
|
|
208
|
+
font-size: 14px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.comment-time {
|
|
212
|
+
font-size: 12px;
|
|
213
|
+
color: #757575;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.comment-text {
|
|
217
|
+
color: #424242;
|
|
218
|
+
line-height: 1.5;
|
|
219
|
+
font-size: 14px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* Responsive Design */
|
|
223
|
+
@media (max-width: 768px) {
|
|
224
|
+
.comment-icon-container {
|
|
225
|
+
top: 16px;
|
|
226
|
+
right: 16px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.comment-icon-button {
|
|
230
|
+
width: 48px;
|
|
231
|
+
height: 48px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.comment-count-badge {
|
|
235
|
+
width: 18px;
|
|
236
|
+
height: 18px;
|
|
237
|
+
font-size: 11px;
|
|
238
|
+
top: -6px;
|
|
239
|
+
right: -6px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.popup-overlay {
|
|
243
|
+
padding: 16px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.popup-container {
|
|
247
|
+
max-height: 90vh;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.popup-header {
|
|
251
|
+
padding: 16px 16px 0;
|
|
252
|
+
margin-bottom: 16px;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.popup-title {
|
|
256
|
+
font-size: 18px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.popup-content {
|
|
260
|
+
padding: 0 16px 16px;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.comments-section {
|
|
264
|
+
padding-top: 16px;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* Dark mode support */
|
|
269
|
+
@media (prefers-color-scheme: dark) {
|
|
270
|
+
.popup-container {
|
|
271
|
+
background: #303030;
|
|
272
|
+
color: #fff;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.popup-title {
|
|
276
|
+
color: #fff;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.popup-header {
|
|
280
|
+
border-bottom-color: #424242;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.form-label {
|
|
284
|
+
color: #e0e0e0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.form-input,
|
|
288
|
+
.form-textarea {
|
|
289
|
+
background: #424242;
|
|
290
|
+
border-color: #616161;
|
|
291
|
+
color: #fff;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.form-input:focus,
|
|
295
|
+
.form-textarea:focus {
|
|
296
|
+
border-color: #42a5f5;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.form-input::placeholder,
|
|
300
|
+
.form-textarea::placeholder {
|
|
301
|
+
color: #bdbdbd;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.close-button:hover {
|
|
305
|
+
background: #424242;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.comments-section {
|
|
309
|
+
border-top-color: #424242;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.comments-title {
|
|
313
|
+
color: #fff;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.comment-item {
|
|
317
|
+
background: #424242;
|
|
318
|
+
border-left-color: #42a5f5;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.comment-email {
|
|
322
|
+
color: #42a5f5;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.comment-text {
|
|
326
|
+
color: #e0e0e0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.comment-count-badge {
|
|
330
|
+
border-color: #303030;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- Floating Comment Icon -->
|
|
3
|
+
<div class="comment-icon-container">
|
|
4
|
+
<dx-button
|
|
5
|
+
icon-symbol={iconSymbol}
|
|
6
|
+
icon-size={iconSize}
|
|
7
|
+
variant="base"
|
|
8
|
+
onclick={handleIconClick}
|
|
9
|
+
aria-label="Open comment popup"
|
|
10
|
+
class="comment-icon-button"
|
|
11
|
+
></dx-button>
|
|
12
|
+
<div if:true={showCommentCount} class="comment-count-badge">
|
|
13
|
+
{commentCount}
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<!-- Popup Overlay -->
|
|
18
|
+
<div if:true={open} class="popup-overlay" onclick={handleOverlayClick}>
|
|
19
|
+
<div
|
|
20
|
+
class="popup-container"
|
|
21
|
+
role="dialog"
|
|
22
|
+
aria-modal="true"
|
|
23
|
+
aria-labelledby="popup-title"
|
|
24
|
+
>
|
|
25
|
+
<!-- Popup Header -->
|
|
26
|
+
<div class="popup-header">
|
|
27
|
+
<h3 id="popup-title" class="popup-title">{popupTitle}</h3>
|
|
28
|
+
<dx-button
|
|
29
|
+
icon-symbol="close"
|
|
30
|
+
icon-size="small"
|
|
31
|
+
variant="base"
|
|
32
|
+
onclick={handleClose}
|
|
33
|
+
aria-label="Close popup"
|
|
34
|
+
class="close-button"
|
|
35
|
+
></dx-button>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<!-- Comment Form -->
|
|
39
|
+
<div class="popup-content">
|
|
40
|
+
<form class="comment-form" onsubmit={handleSubmit}>
|
|
41
|
+
<div class="form-group">
|
|
42
|
+
<label for="email-input" class="form-label">
|
|
43
|
+
Email
|
|
44
|
+
</label>
|
|
45
|
+
<input
|
|
46
|
+
id="email-input"
|
|
47
|
+
type="email"
|
|
48
|
+
class="form-input"
|
|
49
|
+
placeholder={emailPlaceholder}
|
|
50
|
+
value={email}
|
|
51
|
+
oninput={handleEmailChange}
|
|
52
|
+
required
|
|
53
|
+
/>
|
|
54
|
+
<div if:true={emailError} class="error-message">
|
|
55
|
+
{emailError}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="form-group">
|
|
60
|
+
<label for="comment-input" class="form-label">
|
|
61
|
+
Comment
|
|
62
|
+
</label>
|
|
63
|
+
<textarea
|
|
64
|
+
id="comment-input"
|
|
65
|
+
class="form-textarea"
|
|
66
|
+
placeholder={commentPlaceholder}
|
|
67
|
+
value={comment}
|
|
68
|
+
oninput={handleCommentChange}
|
|
69
|
+
rows="4"
|
|
70
|
+
required
|
|
71
|
+
></textarea>
|
|
72
|
+
<div if:true={commentError} class="error-message">
|
|
73
|
+
{commentError}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="form-actions">
|
|
78
|
+
<dx-button
|
|
79
|
+
type="submit"
|
|
80
|
+
variant="brand"
|
|
81
|
+
disabled={submitButtonDisabled}
|
|
82
|
+
onclick={handleSubmit}
|
|
83
|
+
>
|
|
84
|
+
<span if:false={isSubmitting}>
|
|
85
|
+
{submitButtonLabel}
|
|
86
|
+
</span>
|
|
87
|
+
<span if:true={isSubmitting}>Posting...</span>
|
|
88
|
+
</dx-button>
|
|
89
|
+
</div>
|
|
90
|
+
</form>
|
|
91
|
+
|
|
92
|
+
<!-- Comments List -->
|
|
93
|
+
<div if:true={showComments} class="comments-section">
|
|
94
|
+
<h4 class="comments-title">Comments</h4>
|
|
95
|
+
<div class="comments-list">
|
|
96
|
+
<template
|
|
97
|
+
for:each={formattedComments}
|
|
98
|
+
for:item="comment"
|
|
99
|
+
>
|
|
100
|
+
<div key={comment.email} class="comment-item">
|
|
101
|
+
<div class="comment-header">
|
|
102
|
+
<span class="comment-email">
|
|
103
|
+
{comment.maskedEmail}
|
|
104
|
+
</span>
|
|
105
|
+
<span class="comment-time">
|
|
106
|
+
{comment.formattedTimestamp}
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="comment-text">
|
|
110
|
+
{comment.comment}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</template>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</template>
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { LightningElement, api, track } from "lwc";
|
|
2
|
+
import { normalizeBoolean } from "dxUtils/normalizers";
|
|
3
|
+
|
|
4
|
+
interface Comment {
|
|
5
|
+
email: string;
|
|
6
|
+
comment: string;
|
|
7
|
+
url: string;
|
|
8
|
+
timestamp: Date;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default class CommentPopup extends LightningElement {
|
|
12
|
+
@api iconSymbol = "chat";
|
|
13
|
+
@api iconSize = "medium";
|
|
14
|
+
@api popupTitle = "Leave a Comment";
|
|
15
|
+
@api submitButtonLabel = "Post Comment";
|
|
16
|
+
@api emailPlaceholder = "Enter your email";
|
|
17
|
+
@api commentPlaceholder = "Enter your comment";
|
|
18
|
+
|
|
19
|
+
private _open = false;
|
|
20
|
+
@api get open() {
|
|
21
|
+
return this._open;
|
|
22
|
+
}
|
|
23
|
+
set open(value) {
|
|
24
|
+
this._open = normalizeBoolean(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@track private comments: Comment[] = [];
|
|
28
|
+
@track private email = "";
|
|
29
|
+
@track private comment = "";
|
|
30
|
+
@track private emailError = "";
|
|
31
|
+
@track private commentError = "";
|
|
32
|
+
@track private isSubmitting = false;
|
|
33
|
+
|
|
34
|
+
get showComments() {
|
|
35
|
+
const unwrapped = JSON.parse(JSON.stringify(this.comments));
|
|
36
|
+
console.log("Comments (JSON unwrapped):", unwrapped);
|
|
37
|
+
return this.comments.length > 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get commentCount() {
|
|
41
|
+
return this.comments.length;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get showCommentCount() {
|
|
45
|
+
return this.commentCount > 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get sortedComments() {
|
|
49
|
+
return [...this.comments].sort(
|
|
50
|
+
(a, b) => b.timestamp.getTime() - a.timestamp.getTime()
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get isFormValid() {
|
|
55
|
+
return this.email.trim() !== "" && this.comment.trim() !== "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get submitButtonDisabled() {
|
|
59
|
+
return !this.isFormValid || this.isSubmitting;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
handleIconClick() {
|
|
63
|
+
this._open = !this._open;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
handleEmailChange(event: Event) {
|
|
67
|
+
this.email = (event.target as HTMLInputElement).value;
|
|
68
|
+
this.emailError = "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
handleCommentChange(event: Event) {
|
|
72
|
+
this.comment = (event.target as HTMLTextAreaElement).value;
|
|
73
|
+
this.commentError = "";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
handleSubmit() {
|
|
77
|
+
if (!this.validateForm()) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.isSubmitting = true;
|
|
82
|
+
|
|
83
|
+
// Simulate API call
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
this.addComment();
|
|
86
|
+
this.resetForm();
|
|
87
|
+
this.isSubmitting = false;
|
|
88
|
+
}, 500);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private validateForm(): boolean {
|
|
92
|
+
let isValid = true;
|
|
93
|
+
|
|
94
|
+
if (!this.email.trim()) {
|
|
95
|
+
this.emailError = "Email is required";
|
|
96
|
+
isValid = false;
|
|
97
|
+
} else if (!this.isValidEmail(this.email)) {
|
|
98
|
+
this.emailError = "Please enter a valid email address";
|
|
99
|
+
isValid = false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!this.comment.trim()) {
|
|
103
|
+
this.commentError = "Comment is required";
|
|
104
|
+
isValid = false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return isValid;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private isValidEmail(email: string): boolean {
|
|
111
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
112
|
+
return emailRegex.test(email);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private addComment() {
|
|
116
|
+
const newComment: Comment = {
|
|
117
|
+
email: this.email.trim(),
|
|
118
|
+
comment: this.comment.trim(),
|
|
119
|
+
url: window.location.href,
|
|
120
|
+
timestamp: new Date()
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
this.comments = [newComment, ...this.comments];
|
|
124
|
+
|
|
125
|
+
// Dispatch custom event
|
|
126
|
+
this.dispatchEvent(
|
|
127
|
+
new CustomEvent("commentadded", {
|
|
128
|
+
detail: newComment
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private resetForm() {
|
|
134
|
+
this.email = "";
|
|
135
|
+
this.comment = "";
|
|
136
|
+
this.emailError = "";
|
|
137
|
+
this.commentError = "";
|
|
138
|
+
|
|
139
|
+
// Force DOM update by accessing the form elements
|
|
140
|
+
const emailInput = this.template.querySelector(
|
|
141
|
+
"#email-input"
|
|
142
|
+
) as HTMLInputElement;
|
|
143
|
+
const commentInput = this.template.querySelector(
|
|
144
|
+
"#comment-input"
|
|
145
|
+
) as HTMLTextAreaElement;
|
|
146
|
+
|
|
147
|
+
if (emailInput) {
|
|
148
|
+
emailInput.value = "";
|
|
149
|
+
}
|
|
150
|
+
if (commentInput) {
|
|
151
|
+
commentInput.value = "";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
handleClose() {
|
|
156
|
+
this._open = false;
|
|
157
|
+
this.resetForm();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
handleOverlayClick(event: Event) {
|
|
161
|
+
if (event.target === event.currentTarget) {
|
|
162
|
+
this.handleClose();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
handleKeyDown(event: KeyboardEvent) {
|
|
167
|
+
if (event.key === "Escape") {
|
|
168
|
+
this.handleClose();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
connectedCallback() {
|
|
173
|
+
document.addEventListener("keydown", this.handleKeyDown.bind(this));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
disconnectedCallback() {
|
|
177
|
+
document.removeEventListener("keydown", this.handleKeyDown.bind(this));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private formatTimestamp(timestamp: Date): string {
|
|
181
|
+
const now = new Date();
|
|
182
|
+
const diff = now.getTime() - timestamp.getTime();
|
|
183
|
+
const minutes = Math.floor(diff / 60000);
|
|
184
|
+
const hours = Math.floor(diff / 3600000);
|
|
185
|
+
const days = Math.floor(diff / 86400000);
|
|
186
|
+
|
|
187
|
+
if (minutes < 1) {
|
|
188
|
+
return "Just now";
|
|
189
|
+
} else if (minutes < 60) {
|
|
190
|
+
return `${minutes} minute${minutes > 1 ? "s" : ""} ago`;
|
|
191
|
+
} else if (hours < 24) {
|
|
192
|
+
return `${hours} hour${hours > 1 ? "s" : ""} ago`;
|
|
193
|
+
}
|
|
194
|
+
return `${days} day${days > 1 ? "s" : ""} ago`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
get formattedComments() {
|
|
198
|
+
return this.sortedComments.map((comment) => ({
|
|
199
|
+
...comment,
|
|
200
|
+
formattedTimestamp: this.formatTimestamp(comment.timestamp),
|
|
201
|
+
maskedEmail: this.maskEmail(comment.email)
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private maskEmail(email: string): string {
|
|
206
|
+
const [localPart, domain] = email.split("@");
|
|
207
|
+
const maskedLocal =
|
|
208
|
+
localPart.length > 2
|
|
209
|
+
? localPart.substring(0, 2) + "*".repeat(localPart.length - 2)
|
|
210
|
+
: localPart;
|
|
211
|
+
return `${maskedLocal}@${domain}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
.markdown-editor {
|
|
2
|
-
border: 1px solid #e1e5e9;
|
|
3
|
-
border-radius: 4px;
|
|
4
|
-
overflow: hidden;
|
|
5
|
-
background: #fff;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/* Editor Toolbar */
|
|
9
|
-
.editor-toolbar {
|
|
10
|
-
display: flex;
|
|
11
|
-
align-items: center;
|
|
12
|
-
justify-content: flex-end;
|
|
13
|
-
gap: 0.5rem;
|
|
14
|
-
padding: 0.75rem 1rem;
|
|
15
|
-
background: #f8f9fa;
|
|
16
|
-
border-bottom: 1px solid #e1e5e9;
|
|
17
|
-
flex-wrap: wrap;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
.edit-button,
|
|
21
|
-
.cancel-button,
|
|
22
|
-
.save-button {
|
|
23
|
-
min-width: 80px;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
.status-indicator {
|
|
27
|
-
margin-left: 0.5rem;
|
|
28
|
-
font-size: 0.875rem;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.status-saving {
|
|
32
|
-
color: #0176d3;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.status-success {
|
|
36
|
-
color: #04844b;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.status-error {
|
|
40
|
-
color: #ea001e;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/* Content Viewer */
|
|
44
|
-
.content-viewer {
|
|
45
|
-
padding: 1rem;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
.content-display {
|
|
49
|
-
line-height: 1.6;
|
|
50
|
-
color: #3e3e3c;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.content-display h1,
|
|
54
|
-
.content-display h2,
|
|
55
|
-
.content-display h3,
|
|
56
|
-
.content-display h4,
|
|
57
|
-
.content-display h5,
|
|
58
|
-
.content-display h6 {
|
|
59
|
-
margin-top: 1.5rem;
|
|
60
|
-
margin-bottom: 0.5rem;
|
|
61
|
-
color: #181818;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.content-display p {
|
|
65
|
-
margin-bottom: 1rem;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.content-display ul,
|
|
69
|
-
.content-display ol {
|
|
70
|
-
margin-bottom: 1rem;
|
|
71
|
-
padding-left: 2rem;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.content-display code {
|
|
75
|
-
background-color: #f3f2f2;
|
|
76
|
-
padding: 0.125rem 0.25rem;
|
|
77
|
-
border-radius: 0.25rem;
|
|
78
|
-
font-family: Monaco, Menlo, "Ubuntu Mono", monospace;
|
|
79
|
-
font-size: 0.875rem;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.content-display pre {
|
|
83
|
-
background-color: #f3f2f2;
|
|
84
|
-
padding: 1rem;
|
|
85
|
-
border-radius: 0.25rem;
|
|
86
|
-
overflow-x: auto;
|
|
87
|
-
margin-bottom: 1rem;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
.content-display pre code {
|
|
91
|
-
background-color: transparent;
|
|
92
|
-
padding: 0;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/* Editor Wrapper */
|
|
96
|
-
.editor-wrapper {
|
|
97
|
-
position: relative;
|
|
98
|
-
min-height: 300px;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.simple-editor {
|
|
102
|
-
width: 100%;
|
|
103
|
-
min-height: 300px;
|
|
104
|
-
padding: 1rem;
|
|
105
|
-
border: none;
|
|
106
|
-
outline: none;
|
|
107
|
-
font-family: Monaco, Menlo, "Ubuntu Mono", monospace;
|
|
108
|
-
font-size: 0.875rem;
|
|
109
|
-
line-height: 1.5;
|
|
110
|
-
resize: vertical;
|
|
111
|
-
background: #fff;
|
|
112
|
-
color: #3e3e3c;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.simple-editor:focus {
|
|
116
|
-
outline: none;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
.simple-editor::placeholder {
|
|
120
|
-
color: #706e6b;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/* Loading Overlay */
|
|
124
|
-
.loading-overlay {
|
|
125
|
-
position: absolute;
|
|
126
|
-
top: 0;
|
|
127
|
-
left: 0;
|
|
128
|
-
right: 0;
|
|
129
|
-
bottom: 0;
|
|
130
|
-
background: rgb(255 255 255 / 80%);
|
|
131
|
-
display: flex;
|
|
132
|
-
align-items: center;
|
|
133
|
-
justify-content: center;
|
|
134
|
-
z-index: 1000;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/* Responsive Design */
|
|
138
|
-
@media (max-width: 768px) {
|
|
139
|
-
.editor-toolbar {
|
|
140
|
-
flex-wrap: wrap;
|
|
141
|
-
gap: 0.25rem;
|
|
142
|
-
justify-content: flex-end;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
.status-indicator {
|
|
146
|
-
margin-left: 0.25rem;
|
|
147
|
-
width: auto;
|
|
148
|
-
text-align: right;
|
|
149
|
-
margin-top: 0;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
.simple-editor {
|
|
153
|
-
min-height: 250px;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
.content-viewer {
|
|
157
|
-
padding: 0.75rem;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/* Empty state */
|
|
162
|
-
.content-display:empty::before {
|
|
163
|
-
content: "No content to display";
|
|
164
|
-
color: #706e6b;
|
|
165
|
-
font-style: italic;
|
|
166
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="markdown-editor">
|
|
3
|
-
<!-- Editor Toolbar -->
|
|
4
|
-
<div class="editor-toolbar">
|
|
5
|
-
<template if:true={showViewer}>
|
|
6
|
-
<dx-button
|
|
7
|
-
onclick={handleEdit}
|
|
8
|
-
variant="tertiary"
|
|
9
|
-
class="edit-button"
|
|
10
|
-
>
|
|
11
|
-
Edit Content
|
|
12
|
-
</dx-button>
|
|
13
|
-
</template>
|
|
14
|
-
|
|
15
|
-
<template if:true={showEditor}>
|
|
16
|
-
<dx-button
|
|
17
|
-
onclick={handleCancel}
|
|
18
|
-
variant="secondary"
|
|
19
|
-
class="cancel-button"
|
|
20
|
-
>
|
|
21
|
-
Cancel
|
|
22
|
-
</dx-button>
|
|
23
|
-
|
|
24
|
-
<dx-button
|
|
25
|
-
onclick={handleSave}
|
|
26
|
-
variant="primary"
|
|
27
|
-
disabled={saveButtonDisabled}
|
|
28
|
-
class="save-button"
|
|
29
|
-
>
|
|
30
|
-
Save
|
|
31
|
-
</dx-button>
|
|
32
|
-
|
|
33
|
-
<div class="status-indicator">
|
|
34
|
-
<span class={statusClass}>{statusMessage}</span>
|
|
35
|
-
</div>
|
|
36
|
-
</template>
|
|
37
|
-
</div>
|
|
38
|
-
|
|
39
|
-
<!-- Content Viewer (Read-only) -->
|
|
40
|
-
<template if:true={showViewer}>
|
|
41
|
-
<div class="content-viewer">
|
|
42
|
-
<div class="content-display" innerhtml={content}></div>
|
|
43
|
-
</div>
|
|
44
|
-
</template>
|
|
45
|
-
|
|
46
|
-
<!-- Simple Editor -->
|
|
47
|
-
<template if:true={showEditor}>
|
|
48
|
-
<div class="editor-wrapper">
|
|
49
|
-
<textarea
|
|
50
|
-
class="simple-editor"
|
|
51
|
-
value={markdownContent}
|
|
52
|
-
onchange={handleContentChange}
|
|
53
|
-
placeholder="Enter your markdown content here..."
|
|
54
|
-
></textarea>
|
|
55
|
-
</div>
|
|
56
|
-
</template>
|
|
57
|
-
|
|
58
|
-
<!-- Loading Overlay -->
|
|
59
|
-
<template if:true={isLoading}>
|
|
60
|
-
<div class="loading-overlay">
|
|
61
|
-
<dx-spinner
|
|
62
|
-
size="medium"
|
|
63
|
-
alternative-text="Loading"
|
|
64
|
-
></dx-spinner>
|
|
65
|
-
</div>
|
|
66
|
-
</template>
|
|
67
|
-
</div>
|
|
68
|
-
</template>
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { LightningElement, api, track } from "lwc";
|
|
2
|
-
|
|
3
|
-
export default class MarkdownEditor extends LightningElement {
|
|
4
|
-
@api content: string = "";
|
|
5
|
-
@api filePath: string = "";
|
|
6
|
-
@api saveEndpoint: string = "";
|
|
7
|
-
@api authToken: string = "";
|
|
8
|
-
|
|
9
|
-
@track isEditing: boolean = false;
|
|
10
|
-
@track markdownContent: string = "";
|
|
11
|
-
@track isLoading: boolean = false;
|
|
12
|
-
@track saveStatus: "idle" | "saving" | "success" | "error" = "idle";
|
|
13
|
-
|
|
14
|
-
connectedCallback() {
|
|
15
|
-
this.markdownContent = this.content; // Simple fallback for now
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Event Handlers
|
|
19
|
-
handleEdit() {
|
|
20
|
-
this.isEditing = true;
|
|
21
|
-
this.saveStatus = "idle";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
handleCancel() {
|
|
25
|
-
this.isEditing = false;
|
|
26
|
-
this.saveStatus = "idle";
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async handleSave() {
|
|
30
|
-
if (!this.saveEndpoint) {
|
|
31
|
-
console.error("Save endpoint not configured");
|
|
32
|
-
this.saveStatus = "error";
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
this.saveStatus = "saving";
|
|
37
|
-
this.isLoading = true;
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const response = await fetch(this.saveEndpoint, {
|
|
41
|
-
method: "POST",
|
|
42
|
-
headers: {
|
|
43
|
-
"Content-Type": "application/json",
|
|
44
|
-
Authorization: this.authToken
|
|
45
|
-
? `Bearer ${this.authToken}`
|
|
46
|
-
: ""
|
|
47
|
-
},
|
|
48
|
-
body: JSON.stringify({
|
|
49
|
-
content: this.markdownContent,
|
|
50
|
-
filePath: this.filePath,
|
|
51
|
-
originalContent: this.content
|
|
52
|
-
})
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
if (response.ok) {
|
|
56
|
-
this.saveStatus = "success";
|
|
57
|
-
this.isEditing = false;
|
|
58
|
-
|
|
59
|
-
// Dispatch save success event
|
|
60
|
-
this.dispatchEvent(
|
|
61
|
-
new CustomEvent("saved", {
|
|
62
|
-
detail: {
|
|
63
|
-
success: true,
|
|
64
|
-
content: this.markdownContent,
|
|
65
|
-
filePath: this.filePath
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
);
|
|
69
|
-
} else {
|
|
70
|
-
throw new Error(`Save failed: ${response.status}`);
|
|
71
|
-
}
|
|
72
|
-
} catch (error) {
|
|
73
|
-
console.error("Save failed:", error);
|
|
74
|
-
this.saveStatus = "error";
|
|
75
|
-
} finally {
|
|
76
|
-
this.isLoading = false;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Getters for UI state
|
|
81
|
-
get showEditor() {
|
|
82
|
-
return this.isEditing;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
get showViewer() {
|
|
86
|
-
return !this.isEditing;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
get saveButtonDisabled() {
|
|
90
|
-
return this.saveStatus === "saving" || this.isLoading;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
get statusMessage() {
|
|
94
|
-
switch (this.saveStatus) {
|
|
95
|
-
case "saving":
|
|
96
|
-
return "Saving...";
|
|
97
|
-
case "success":
|
|
98
|
-
return "Saved successfully!";
|
|
99
|
-
case "error":
|
|
100
|
-
return "Save failed. Please try again.";
|
|
101
|
-
default:
|
|
102
|
-
return "";
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
get statusClass() {
|
|
107
|
-
switch (this.saveStatus) {
|
|
108
|
-
case "saving":
|
|
109
|
-
return "status-saving";
|
|
110
|
-
case "success":
|
|
111
|
-
return "status-success";
|
|
112
|
-
case "error":
|
|
113
|
-
return "status-error";
|
|
114
|
-
default:
|
|
115
|
-
return "";
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|