create-template-html-css 2.0.3 → 2.1.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/CHANGELOG.md +305 -0
- package/HTML-VS-REACT.md +289 -0
- package/QUICKSTART-REACT.md +293 -0
- package/REACT-SUPPORT-SUMMARY.md +235 -0
- package/README.md +193 -12
- package/bin/cli.js +98 -759
- package/bin/commands/create.js +272 -0
- package/bin/commands/gallery.js +42 -0
- package/bin/commands/insert.js +123 -0
- package/bin/commands/list.js +73 -0
- package/package.json +10 -3
- package/src/component-choices.js +7 -0
- package/src/components-registry.js +112 -0
- package/src/format-utils.js +49 -0
- package/src/generator.js +83 -594
- package/src/generators/color-schemes.js +78 -0
- package/src/generators/color-utils.js +108 -0
- package/src/generators/component-filters.js +151 -0
- package/src/generators/html-generators.js +180 -0
- package/src/generators/validation.js +43 -0
- package/src/index.js +2 -1
- package/src/inserter.js +55 -233
- package/src/inserters/backup-utils.js +20 -0
- package/src/inserters/component-loader.js +68 -0
- package/src/inserters/html-utils.js +31 -0
- package/src/inserters/indentation-utils.js +90 -0
- package/src/inserters/validation-utils.js +49 -0
- package/src/react-component-choices.js +45 -0
- package/src/react-file-operations.js +172 -0
- package/src/react-generator.js +208 -0
- package/src/react-templates.js +350 -0
- package/src/utils/file-utils.js +97 -0
- package/src/utils/path-utils.js +32 -0
- package/src/utils/string-utils.js +51 -0
- package/src/utils/template-loader.js +91 -0
- package/templates/_shared/PATTERNS.md +246 -0
- package/templates/_shared/README.md +74 -0
- package/templates/_shared/base.css +18 -0
- package/templates/blackjack/index.html +1 -1
- package/templates/blackjack/script.js +9 -9
- package/templates/breakout/index.html +1 -1
- package/templates/breakout/script.js +6 -6
- package/templates/connect-four/index.html +1 -1
- package/templates/connect-four/script.js +5 -5
- package/templates/dice-game/index.html +1 -1
- package/templates/dice-game/script.js +20 -20
- package/templates/flappy-bird/index.html +1 -1
- package/templates/flappy-bird/script.js +10 -10
- package/templates/pong/index.html +1 -1
- package/templates/pong/script.js +8 -8
- package/templates/skeleton/index.html +4 -4
- package/templates/slot-machine/index.html +1 -1
- package/templates/slot-machine/script.js +6 -6
- package/templates/tetris/index.html +1 -1
- package/templates/tetris/script.js +5 -5
- package/templates-react/README.md +126 -0
- package/templates-react/button/Button.css +88 -0
- package/templates-react/button/Button.example.jsx +40 -0
- package/templates-react/button/Button.jsx +29 -0
- package/templates-react/card/Card.css +86 -0
- package/templates-react/card/Card.example.jsx +49 -0
- package/templates-react/card/Card.jsx +35 -0
- package/templates-react/counter/Counter.css +99 -0
- package/templates-react/counter/Counter.example.jsx +45 -0
- package/templates-react/counter/Counter.jsx +70 -0
- package/templates-react/form/Form.css +128 -0
- package/templates-react/form/Form.example.jsx +65 -0
- package/templates-react/form/Form.jsx +125 -0
- package/templates-react/modal/Modal.css +152 -0
- package/templates-react/modal/Modal.example.jsx +90 -0
- package/templates-react/modal/Modal.jsx +46 -0
- package/templates-react/todo-list/TodoList.css +236 -0
- package/templates-react/todo-list/TodoList.example.jsx +15 -0
- package/templates-react/todo-list/TodoList.jsx +84 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import Modal from './Modal';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Example usage of Modal component
|
|
6
|
+
*/
|
|
7
|
+
const ModalExample = () => {
|
|
8
|
+
const [isSmallOpen, setIsSmallOpen] = useState(false);
|
|
9
|
+
const [isMediumOpen, setIsMediumOpen] = useState(false);
|
|
10
|
+
const [isLargeOpen, setIsLargeOpen] = useState(false);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div style={{ padding: '40px' }}>
|
|
14
|
+
<h2 style={{ marginBottom: '30px' }}>Modal Component Examples</h2>
|
|
15
|
+
|
|
16
|
+
<div style={{ display: 'flex', gap: '15px', flexWrap: 'wrap' }}>
|
|
17
|
+
<button
|
|
18
|
+
onClick={() => setIsSmallOpen(true)}
|
|
19
|
+
style={{ padding: '12px 24px', background: '#3182ce', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }}
|
|
20
|
+
>
|
|
21
|
+
Open Small Modal
|
|
22
|
+
</button>
|
|
23
|
+
|
|
24
|
+
<button
|
|
25
|
+
onClick={() => setIsMediumOpen(true)}
|
|
26
|
+
style={{ padding: '12px 24px', background: '#3182ce', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }}
|
|
27
|
+
>
|
|
28
|
+
Open Medium Modal
|
|
29
|
+
</button>
|
|
30
|
+
|
|
31
|
+
<button
|
|
32
|
+
onClick={() => setIsLargeOpen(true)}
|
|
33
|
+
style={{ padding: '12px 24px', background: '#3182ce', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }}
|
|
34
|
+
>
|
|
35
|
+
Open Large Modal
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<Modal
|
|
40
|
+
isOpen={isSmallOpen}
|
|
41
|
+
onClose={() => setIsSmallOpen(false)}
|
|
42
|
+
title="Small Modal"
|
|
43
|
+
size="small"
|
|
44
|
+
footer={
|
|
45
|
+
<>
|
|
46
|
+
<button
|
|
47
|
+
onClick={() => setIsSmallOpen(false)}
|
|
48
|
+
style={{ padding: '10px 20px', background: '#e2e8f0', border: 'none', borderRadius: '6px', cursor: 'pointer' }}
|
|
49
|
+
>
|
|
50
|
+
Cancel
|
|
51
|
+
</button>
|
|
52
|
+
<button
|
|
53
|
+
onClick={() => { console.log('Confirmed!'); setIsSmallOpen(false); }}
|
|
54
|
+
style={{ padding: '10px 20px', background: '#3182ce', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer' }}
|
|
55
|
+
>
|
|
56
|
+
Confirm
|
|
57
|
+
</button>
|
|
58
|
+
</>
|
|
59
|
+
}
|
|
60
|
+
>
|
|
61
|
+
<p>This is a small modal perfect for confirmation dialogs or short messages.</p>
|
|
62
|
+
</Modal>
|
|
63
|
+
|
|
64
|
+
<Modal
|
|
65
|
+
isOpen={isMediumOpen}
|
|
66
|
+
onClose={() => setIsMediumOpen(false)}
|
|
67
|
+
title="Medium Modal"
|
|
68
|
+
size="medium"
|
|
69
|
+
>
|
|
70
|
+
<h3>Modal Content</h3>
|
|
71
|
+
<p>This is a medium-sized modal suitable for forms or detailed information.</p>
|
|
72
|
+
<p>You can include any React components or HTML elements inside the modal.</p>
|
|
73
|
+
</Modal>
|
|
74
|
+
|
|
75
|
+
<Modal
|
|
76
|
+
isOpen={isLargeOpen}
|
|
77
|
+
onClose={() => setIsLargeOpen(false)}
|
|
78
|
+
title="Large Modal"
|
|
79
|
+
size="large"
|
|
80
|
+
>
|
|
81
|
+
<h3>Large Modal Content</h3>
|
|
82
|
+
<p>This is a large modal ideal for complex interfaces or detailed content.</p>
|
|
83
|
+
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
|
84
|
+
<p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
|
|
85
|
+
</Modal>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export default ModalExample;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import './Modal.css';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Modal Component
|
|
6
|
+
* A flexible modal dialog component
|
|
7
|
+
*/
|
|
8
|
+
const Modal = ({
|
|
9
|
+
isOpen,
|
|
10
|
+
onClose,
|
|
11
|
+
title,
|
|
12
|
+
children,
|
|
13
|
+
footer,
|
|
14
|
+
showCloseButton = true,
|
|
15
|
+
closeOnOverlayClick = true,
|
|
16
|
+
size = 'medium'
|
|
17
|
+
}) => {
|
|
18
|
+
if (!isOpen) return null;
|
|
19
|
+
|
|
20
|
+
const handleOverlayClick = (e) => {
|
|
21
|
+
if (closeOnOverlayClick && e.target === e.currentTarget) {
|
|
22
|
+
onClose?.();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="modal-overlay" onClick={handleOverlayClick}>
|
|
28
|
+
<div className={`modal-content modal-${size}`}>
|
|
29
|
+
<div className="modal-header">
|
|
30
|
+
{title && <h2 className="modal-title">{title}</h2>}
|
|
31
|
+
{showCloseButton && (
|
|
32
|
+
<button className="modal-close" onClick={onClose} aria-label="Close">
|
|
33
|
+
×
|
|
34
|
+
</button>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
<div className="modal-body">
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
40
|
+
{footer && <div className="modal-footer">{footer}</div>}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default Modal;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/* Todo List Styles */
|
|
2
|
+
.todo-container {
|
|
3
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
4
|
+
background: white;
|
|
5
|
+
padding: 40px;
|
|
6
|
+
border-radius: 20px;
|
|
7
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
|
8
|
+
max-width: 600px;
|
|
9
|
+
margin: 0 auto;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.todo-title {
|
|
13
|
+
font-size: 32px;
|
|
14
|
+
font-weight: 700;
|
|
15
|
+
margin: 0 0 30px 0;
|
|
16
|
+
text-align: center;
|
|
17
|
+
background: linear-gradient(135deg, {{primaryColor}} 0%, {{secondaryColor}} 100%);
|
|
18
|
+
-webkit-background-clip: text;
|
|
19
|
+
-webkit-text-fill-color: transparent;
|
|
20
|
+
background-clip: text;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.todo-form {
|
|
24
|
+
display: flex;
|
|
25
|
+
gap: 12px;
|
|
26
|
+
margin-bottom: 24px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.todo-input {
|
|
30
|
+
flex: 1;
|
|
31
|
+
padding: 14px 20px;
|
|
32
|
+
font-size: 16px;
|
|
33
|
+
font-family: inherit;
|
|
34
|
+
border: 2px solid #e2e8f0;
|
|
35
|
+
border-radius: 12px;
|
|
36
|
+
transition: all 0.3s ease;
|
|
37
|
+
outline: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.todo-input:focus {
|
|
41
|
+
border-color: {{primaryColor}};
|
|
42
|
+
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.todo-add-btn {
|
|
46
|
+
padding: 14px 32px;
|
|
47
|
+
font-size: 16px;
|
|
48
|
+
font-weight: 600;
|
|
49
|
+
font-family: inherit;
|
|
50
|
+
background: linear-gradient(135deg, {{primaryColor}} 0%, {{secondaryColor}} 100%);
|
|
51
|
+
color: white;
|
|
52
|
+
border: none;
|
|
53
|
+
border-radius: 12px;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
transition: all 0.3s ease;
|
|
56
|
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.todo-add-btn:hover {
|
|
60
|
+
transform: translateY(-2px);
|
|
61
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.todo-add-btn:active {
|
|
65
|
+
transform: translateY(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.todo-stats {
|
|
69
|
+
display: flex;
|
|
70
|
+
justify-content: space-around;
|
|
71
|
+
margin-bottom: 24px;
|
|
72
|
+
padding: 16px;
|
|
73
|
+
background: #f7fafc;
|
|
74
|
+
border-radius: 12px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.todo-stat {
|
|
78
|
+
font-size: 14px;
|
|
79
|
+
font-weight: 600;
|
|
80
|
+
color: #4a5568;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.todo-list {
|
|
84
|
+
list-style: none;
|
|
85
|
+
padding: 0;
|
|
86
|
+
margin: 0;
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
gap: 12px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.todo-empty {
|
|
93
|
+
padding: 40px 20px;
|
|
94
|
+
text-align: center;
|
|
95
|
+
color: #a0aec0;
|
|
96
|
+
font-size: 18px;
|
|
97
|
+
list-style: none;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.todo-item {
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
justify-content: space-between;
|
|
104
|
+
padding: 16px 20px;
|
|
105
|
+
background: #f7fafc;
|
|
106
|
+
border-radius: 12px;
|
|
107
|
+
transition: all 0.3s ease;
|
|
108
|
+
animation: slideIn 0.3s ease;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@keyframes slideIn {
|
|
112
|
+
from {
|
|
113
|
+
opacity: 0;
|
|
114
|
+
transform: translateX(-20px);
|
|
115
|
+
}
|
|
116
|
+
to {
|
|
117
|
+
opacity: 1;
|
|
118
|
+
transform: translateX(0);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.todo-item:hover {
|
|
123
|
+
background: #edf2f7;
|
|
124
|
+
transform: translateX(5px);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.todo-item.completed {
|
|
128
|
+
opacity: 0.6;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.todo-content {
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
gap: 16px;
|
|
135
|
+
flex: 1;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
user-select: none;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.todo-checkbox {
|
|
141
|
+
width: 24px;
|
|
142
|
+
height: 24px;
|
|
143
|
+
border: 2px solid {{primaryColor}};
|
|
144
|
+
border-radius: 6px;
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
148
|
+
flex-shrink: 0;
|
|
149
|
+
transition: all 0.3s ease;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.todo-item.completed .todo-checkbox {
|
|
153
|
+
background: linear-gradient(135deg, {{primaryColor}} 0%, {{secondaryColor}} 100%);
|
|
154
|
+
border-color: transparent;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.todo-checkmark {
|
|
158
|
+
color: white;
|
|
159
|
+
font-size: 16px;
|
|
160
|
+
font-weight: bold;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.todo-text {
|
|
164
|
+
font-size: 16px;
|
|
165
|
+
color: #2d3748;
|
|
166
|
+
transition: all 0.3s ease;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.todo-item.completed .todo-text {
|
|
170
|
+
text-decoration: line-through;
|
|
171
|
+
color: #a0aec0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.todo-delete-btn {
|
|
175
|
+
width: 32px;
|
|
176
|
+
height: 32px;
|
|
177
|
+
background: #fee;
|
|
178
|
+
color: #e53e3e;
|
|
179
|
+
border: none;
|
|
180
|
+
border-radius: 8px;
|
|
181
|
+
font-size: 24px;
|
|
182
|
+
line-height: 1;
|
|
183
|
+
cursor: pointer;
|
|
184
|
+
transition: all 0.3s ease;
|
|
185
|
+
display: flex;
|
|
186
|
+
align-items: center;
|
|
187
|
+
justify-content: center;
|
|
188
|
+
flex-shrink: 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.todo-delete-btn:hover {
|
|
192
|
+
background: #e53e3e;
|
|
193
|
+
color: white;
|
|
194
|
+
transform: scale(1.1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Dark Mode Support */
|
|
198
|
+
@media (prefers-color-scheme: dark) {
|
|
199
|
+
.todo-container {
|
|
200
|
+
background: #2d3748;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.todo-input {
|
|
204
|
+
background: #1a202c;
|
|
205
|
+
color: #e2e8f0;
|
|
206
|
+
border-color: #4a5568;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.todo-input:focus {
|
|
210
|
+
border-color: {{primaryColor}};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.todo-stats {
|
|
214
|
+
background: #1a202c;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.todo-stat {
|
|
218
|
+
color: #cbd5e0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.todo-item {
|
|
222
|
+
background: #1a202c;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.todo-item:hover {
|
|
226
|
+
background: #4a5568;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.todo-text {
|
|
230
|
+
color: #e2e8f0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.todo-item.completed .todo-text {
|
|
234
|
+
color: #718096;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import TodoList from './TodoList';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Example usage of TodoList component
|
|
6
|
+
*/
|
|
7
|
+
const TodoListExample = () => {
|
|
8
|
+
return (
|
|
9
|
+
<div style={{ padding: '40px', minHeight: '100vh', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
|
|
10
|
+
<TodoList />
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default TodoListExample;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import './TodoList.css';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TodoList Component
|
|
6
|
+
* A complete todo list with add, toggle, and delete functionality
|
|
7
|
+
*/
|
|
8
|
+
const TodoList = () => {
|
|
9
|
+
const [todos, setTodos] = useState([]);
|
|
10
|
+
const [inputValue, setInputValue] = useState('');
|
|
11
|
+
|
|
12
|
+
const addTodo = (e) => {
|
|
13
|
+
e.preventDefault();
|
|
14
|
+
if (inputValue.trim()) {
|
|
15
|
+
setTodos([...todos, {
|
|
16
|
+
id: Date.now(),
|
|
17
|
+
text: inputValue.trim(),
|
|
18
|
+
completed: false
|
|
19
|
+
}]);
|
|
20
|
+
setInputValue('');
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const toggleTodo = (id) => {
|
|
25
|
+
setTodos(todos.map(todo =>
|
|
26
|
+
todo.id === id ? { ...todo, completed: !todo.completed } : todo
|
|
27
|
+
));
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const deleteTodo = (id) => {
|
|
31
|
+
setTodos(todos.filter(todo => todo.id !== id));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const activeCount = todos.filter(todo => !todo.completed).length;
|
|
35
|
+
const completedCount = todos.filter(todo => todo.completed).length;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="todo-container">
|
|
39
|
+
<h1 className="todo-title">Todo List</h1>
|
|
40
|
+
|
|
41
|
+
<form onSubmit={addTodo} className="todo-form">
|
|
42
|
+
<input
|
|
43
|
+
type="text"
|
|
44
|
+
value={inputValue}
|
|
45
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
46
|
+
placeholder="Add a new task..."
|
|
47
|
+
className="todo-input"
|
|
48
|
+
/>
|
|
49
|
+
<button type="submit" className="todo-add-btn">Add</button>
|
|
50
|
+
</form>
|
|
51
|
+
|
|
52
|
+
<div className="todo-stats">
|
|
53
|
+
<span className="todo-stat">Active: {activeCount}</span>
|
|
54
|
+
<span className="todo-stat">Completed: {completedCount}</span>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<ul className="todo-list">
|
|
58
|
+
{todos.length === 0 ? (
|
|
59
|
+
<li className="todo-empty">No tasks yet. Add one above! 🎯</li>
|
|
60
|
+
) : (
|
|
61
|
+
todos.map(todo => (
|
|
62
|
+
<li key={todo.id} className={`todo-item ${todo.completed ? 'completed' : ''}`}>
|
|
63
|
+
<div className="todo-content" onClick={() => toggleTodo(todo.id)}>
|
|
64
|
+
<div className="todo-checkbox">
|
|
65
|
+
{todo.completed && <span className="todo-checkmark">✓</span>}
|
|
66
|
+
</div>
|
|
67
|
+
<span className="todo-text">{todo.text}</span>
|
|
68
|
+
</div>
|
|
69
|
+
<button
|
|
70
|
+
onClick={() => deleteTodo(todo.id)}
|
|
71
|
+
className="todo-delete-btn"
|
|
72
|
+
aria-label="Delete task"
|
|
73
|
+
>
|
|
74
|
+
×
|
|
75
|
+
</button>
|
|
76
|
+
</li>
|
|
77
|
+
))
|
|
78
|
+
)}
|
|
79
|
+
</ul>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default TodoList;
|