@@ -16,130 +16,236 @@ app.get("/", (c) => {
16
16
< meta name ="viewport " content ="width=device-width, initial-scale=1.0 ">
17
17
< title > Badge Generator</ title >
18
18
< style >
19
+ : root {
20
+ --primary : # 0366d6 ;
21
+ --gray : # 586069 ;
22
+ --border : # e1e4e8 ;
23
+ --input-bg : # f6f8fa ;
24
+ }
25
+ * {
26
+ box-sizing : border-box;
27
+ }
19
28
body {
20
29
font-family : -apple-system, BlinkMacSystemFont, 'Segoe UI' , Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
21
- max-width : 800 px ;
30
+ max-width : 1000 px ;
22
31
margin : 0 auto;
23
32
padding : 20px ;
33
+ background : # fafbfc ;
34
+ color : # 24292e ;
35
+ line-height : 1.5 ;
36
+ }
37
+ h1 {
38
+ text-align : center;
39
+ color : var (--primary );
40
+ margin-bottom : 2rem ;
41
+ }
42
+ .container {
43
+ background : white;
44
+ border-radius : 8px ;
45
+ box-shadow : 0 1px 3px rgba (0 , 0 , 0 , 0.12 );
46
+ padding : 24px ;
24
47
}
25
48
.preview {
26
49
margin : 20px 0 ;
27
- padding : 20px ;
28
- border : 1px solid # ddd ;
29
- border-radius : 4px ;
50
+ padding : 24px ;
51
+ border : 1px solid var (--border );
52
+ border-radius : 8px ;
53
+ text-align : center;
54
+ background : var (--input-bg );
55
+ }
56
+ .preview img {
57
+ max-width : 100% ;
58
+ height : auto;
59
+ transition : opacity 0.3s ;
60
+ }
61
+ .preview .loading img {
62
+ opacity : 0.5 ;
30
63
}
31
64
.controls {
32
65
display : grid;
33
- grid-template-columns : repeat (auto-fill , minmax (200px , 1fr ));
34
- gap : 10 px ;
35
- margin-bottom : 20 px ;
66
+ grid-template-columns : repeat (auto-fit , minmax (200px , 1fr ));
67
+ gap : 16 px ;
68
+ margin-bottom : 24 px ;
36
69
}
37
- .controls label {
70
+ .control-group {
71
+ margin-bottom : 8px ;
72
+ }
73
+ .control-group label {
38
74
display : block;
39
- margin-bottom : 5px ;
75
+ margin-bottom : 8px ;
76
+ font-weight : 500 ;
77
+ color : var (--gray );
40
78
}
41
- . url-box {
79
+ input , select {
42
80
width : 100% ;
43
- padding : 10px ;
44
- margin : 10px 0 ;
45
- font-family : monospace;
81
+ padding : 8px 12px ;
82
+ border : 1px solid var (--border );
83
+ border-radius : 6px ;
84
+ background : var (--input-bg );
85
+ font-size : 14px ;
86
+ transition : border-color 0.2s ;
46
87
}
47
- button {
48
- padding : 8px 16px ;
49
- background : # 0366d6 ;
50
- color : white;
51
- border : none;
52
- border-radius : 4px ;
53
- cursor : pointer;
88
+ input : focus , select : focus {
89
+ border-color : var (--primary );
90
+ outline : none;
91
+ box-shadow : 0 0 0 3px rgba (3 , 102 , 214 , 0.1 );
54
92
}
55
- button : hover {
56
- background : # 0255b3 ;
93
+ .url-box {
94
+ width : 100% ;
95
+ padding : 12px ;
96
+ margin : 16px 0 ;
97
+ font-family : monospace;
98
+ background : var (--input-bg );
99
+ border : 1px solid var (--border );
100
+ border-radius : 6px ;
101
+ font-size : 14px ;
57
102
}
58
103
.tabs {
59
104
display : flex;
60
- gap : 10px ;
61
- margin-bottom : 20px ;
105
+ gap : 8px ;
106
+ margin-bottom : 24px ;
107
+ border-bottom : 1px solid var (--border );
108
+ padding-bottom : 8px ;
62
109
}
63
110
.tabs button {
64
- background : # eee ;
65
- color : # 333 ;
111
+ padding : 8px 16px ;
112
+ background : transparent;
113
+ border : none;
114
+ color : var (--gray );
115
+ cursor : pointer;
116
+ font-size : 16px ;
117
+ font-weight : 500 ;
118
+ border-bottom : 2px solid transparent;
119
+ transition : all 0.2s ;
120
+ }
121
+ .tabs button : hover {
122
+ color : var (--primary );
66
123
}
67
124
.tabs button .active {
68
- background : # 0366d6 ;
125
+ color : var (--primary );
126
+ border-bottom-color : var (--primary );
127
+ }
128
+ .copy-btn {
129
+ display : block;
130
+ width : 100% ;
131
+ padding : 12px ;
132
+ background : var (--primary );
69
133
color : white;
134
+ border : none;
135
+ border-radius : 6px ;
136
+ font-size : 16px ;
137
+ font-weight : 500 ;
138
+ cursor : pointer;
139
+ transition : background 0.2s ;
140
+ }
141
+ .copy-btn : hover {
142
+ background : # 0255b3 ;
143
+ }
144
+ @media (max-width : 600px ) {
145
+ body {
146
+ padding : 12px ;
147
+ }
148
+ .container {
149
+ padding : 16px ;
150
+ }
151
+ .controls {
152
+ grid-template-columns : 1fr ;
153
+ }
70
154
}
71
155
</ style >
72
156
</ head >
73
157
< body >
74
158
< h1 > Badge Generator</ h1 >
75
-
76
- < div class ="tabs ">
77
- < button onclick ="switchTab('visitor') " class ="active "> Visitor Badge</ button >
78
- < button onclick ="switchTab('ai') "> AI Badge</ button >
79
- </ div >
159
+ < div class =" container " >
160
+ < div class ="tabs ">
161
+ < button onclick ="switchTab('visitor') " class ="active "> Visitor Badge</ button >
162
+ < button onclick ="switchTab('ai') "> AI Badge</ button >
163
+ </ div >
80
164
81
- < div id ="visitor-tab ">
82
- < div class ="controls ">
83
- < div >
84
- < label > Repository:</ label >
85
- < input type ="text " id ="repo " placeholder ="username/repo " />
86
- </ div >
87
- < div >
88
- < label > Style:</ label >
89
- < select id ="style ">
90
- < option value ="flat "> Flat</ option >
91
- < option value ="flat-square "> Flat Square</ option >
92
- < option value ="plastic "> Plastic</ option >
93
- < option value ="for-the-badge "> For the Badge</ option >
94
- < option value ="social "> Social</ option >
95
- </ select >
96
- </ div >
97
- < div >
98
- < label > Color:</ label >
99
- < input type ="text " id ="color " value ="blue " />
100
- </ div >
101
- < div >
102
- < label > Label:</ label >
103
- < input type ="text " id ="label " value ="Profile views " />
165
+ < div id ="visitor-tab ">
166
+ < div class ="controls ">
167
+ < div class ="control-group ">
168
+ < label > Repository:</ label >
169
+ < input type ="text " id ="repo " placeholder ="username/repo " />
170
+ </ div >
171
+ < div class ="control-group ">
172
+ < label > Style:</ label >
173
+ < select id ="style ">
174
+ < option value ="flat "> Flat</ option >
175
+ < option value ="flat-square "> Flat Square</ option >
176
+ < option value ="plastic "> Plastic</ option >
177
+ < option value ="for-the-badge "> For the Badge</ option >
178
+ < option value ="social "> Social</ option >
179
+ </ select >
180
+ </ div >
181
+ < div class ="control-group ">
182
+ < label > Color:</ label >
183
+ < input type ="text " id ="color " value ="blue " />
184
+ </ div >
185
+ < div class ="control-group ">
186
+ < label > Label:</ label >
187
+ < input type ="text " id ="label " value ="Profile views " />
188
+ </ div >
104
189
</ div >
105
190
</ div >
106
- </ div >
107
191
108
- < div id ="ai-tab " style ="display:none ">
109
- < div class ="controls ">
110
- < div >
111
- < label > Prompt:</ label >
112
- < input type ="text " id ="prompt " placeholder ="Generate a message... " />
113
- </ div >
114
- < div >
115
- < label > Style:</ label >
116
- < select id ="ai-style ">
117
- < option value ="flat "> Flat</ option >
118
- < option value ="flat-square "> Flat Square</ option >
119
- < option value ="plastic "> Plastic</ option >
120
- < option value ="for-the-badge "> For the Badge</ option >
121
- < option value ="social "> Social</ option >
122
- </ select >
123
- </ div >
124
- < div >
125
- < label > Color:</ label >
126
- < input type ="text " id ="ai-color " value ="blue " />
192
+ < div id ="ai-tab " style ="display:none ">
193
+ < div class ="controls ">
194
+ < div class ="control-group ">
195
+ < label > Prompt:</ label >
196
+ < input type ="text " id ="prompt " placeholder ="Generate a message... " />
197
+ </ div >
198
+ < div class ="control-group ">
199
+ < label > Style:</ label >
200
+ < select id ="ai-style ">
201
+ < option value ="flat "> Flat</ option >
202
+ < option value ="flat-square "> Flat Square</ option >
203
+ < option value ="plastic "> Plastic</ option >
204
+ < option value ="for-the-badge "> For the Badge</ option >
205
+ < option value ="social "> Social</ option >
206
+ </ select >
207
+ </ div >
208
+ < div class ="control-group ">
209
+ < label > Color:</ label >
210
+ < input type ="text " id ="ai-color " value ="blue " />
211
+ </ div >
127
212
</ div >
128
213
</ div >
129
- </ div >
130
214
131
- < div class ="preview ">
132
- < h3 > Preview:</ h3 >
133
- < img id ="preview " src ="" alt ="Badge preview " />
134
- </ div >
215
+ < div class ="preview ">
216
+ < img id ="preview " src ="" alt ="Badge preview " />
217
+ </ div >
135
218
136
- < input type ="text " id ="url " class ="url-box " readonly />
137
- < button onclick ="copyUrl() "> Copy URL</ button >
219
+ < input type ="text " id ="url " class ="url-box " readonly />
220
+ < button onclick ="copyUrl() " class ="copy-btn "> Copy URL</ button >
221
+ </ div >
138
222
139
223
< script >
140
224
let currentTab = 'visitor' ;
225
+ let updateTimeout ;
141
226
142
- function updatePreview ( ) {
227
+ function debounce ( func , wait ) {
228
+ return function executedFunction ( ...args ) {
229
+ const later = ( ) => {
230
+ clearTimeout ( updateTimeout ) ;
231
+ func ( ...args ) ;
232
+ } ;
233
+ clearTimeout ( updateTimeout ) ;
234
+ updateTimeout = setTimeout ( later , wait ) ;
235
+ } ;
236
+ }
237
+
238
+ const setPreviewLoading = ( loading ) => {
239
+ const preview = document . querySelector ( '.preview' ) ;
240
+ if ( loading ) {
241
+ preview . classList . add ( 'loading' ) ;
242
+ } else {
243
+ preview . classList . remove ( 'loading' ) ;
244
+ }
245
+ } ;
246
+
247
+ const debouncedUpdatePreview = debounce ( ( ) => {
248
+ setPreviewLoading ( true ) ;
143
249
const baseUrl = window . location . origin ;
144
250
let url ;
145
251
@@ -158,9 +264,12 @@ app.get("/", (c) => {
158
264
url = \`\${ baseUrl} / ai - badge ?prompt = \${ encodeURIComponent ( prompt ) } & style = \${ style} & color = \${ color} \`;
159
265
}
160
266
161
- document . getElementById ( 'preview' ) . src = url ;
267
+ const previewImg = document . getElementById ( 'preview' ) ;
268
+ previewImg . onload = ( ) => setPreviewLoading ( false ) ;
269
+ previewImg . onerror = ( ) => setPreviewLoading ( false ) ;
270
+ previewImg . src = url ;
162
271
document . getElementById ( 'url' ) . value = url ;
163
- }
272
+ } , 500 ) ;
164
273
165
274
function switchTab ( tab ) {
166
275
currentTab = tab ;
@@ -171,22 +280,29 @@ app.get("/", (c) => {
171
280
tabs . forEach ( btn => btn . classList . remove ( 'active' ) ) ;
172
281
event . target . classList . add ( 'active' ) ;
173
282
174
- updatePreview ( ) ;
283
+ debouncedUpdatePreview ( ) ;
175
284
}
176
285
177
286
function copyUrl ( ) {
178
287
const urlInput = document . getElementById ( 'url' ) ;
179
288
urlInput . select ( ) ;
180
289
document . execCommand ( 'copy' ) ;
290
+
291
+ const btn = document . querySelector ( '.copy-btn' ) ;
292
+ const originalText = btn . textContent ;
293
+ btn . textContent = 'Copied!' ;
294
+ setTimeout ( ( ) => {
295
+ btn . textContent = originalText ;
296
+ } , 2000 ) ;
181
297
}
182
298
183
- // Add event listeners to all inputs
299
+ // Add event listeners to all inputs with debounce
184
300
document . querySelectorAll ( 'input, select' ) . forEach ( input => {
185
- input . addEventListener ( 'input' , updatePreview ) ;
301
+ input . addEventListener ( 'input' , debouncedUpdatePreview ) ;
186
302
} ) ;
187
303
188
304
// Initial preview
189
- updatePreview ( ) ;
305
+ debouncedUpdatePreview ( ) ;
190
306
</ script >
191
307
</ body >
192
308
</ html >
0 commit comments