challenge15-webdev

public
tiara Mar 17, 2026 Never 73
Clone
CSS challenge15-webdev-css 475 lines (432 loc) | 16.54 KB
1
:root {
2
--bg: #f7f4f0;
3
--surface: #fdfcfa;
4
--surface2: #f2ede8;
5
--border: #e8e1d9;
6
--border-focus:#b8a99a;
7
8
--text: #2d2620;
9
--text-2: #6b5f56;
10
--text-3: #a8998e;
11
12
--sage: #8aaa8c;
13
--sage-light: #edf2ed;
14
--sage-mid: #c5d9c6;
15
16
--blush: #c9918a;
17
--blush-light: #f5ebe9;
18
--blush-mid: #e4b8b3;
19
20
--sky: #88a8c0;
21
--sky-light: #e9f0f5;
22
--sky-mid: #b8d0e0;
23
24
--sand: #c4a882;
25
--sand-light: #f5ede0;
26
--sand-mid: #dfc9a8;
27
28
--lavender: #9e92c0;
29
--lavender-light: #eeecf5;
30
--lavender-mid: #ccc5e2;
31
32
--radius: 14px;
33
--radius-lg: 20px;
34
}
35
36
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
37
38
body {
39
background: var(--bg);
40
color: var(--text);
41
font-family: 'DM Sans', sans-serif;
42
min-height: 100vh;
43
}
44
45
body::before {
46
content: '';
47
position: fixed; inset: 0; pointer-events: none; z-index: 0;
48
background:
49
radial-gradient(ellipse 55% 45% at 5% 0%, rgba(138,170,140,.13) 0%, transparent 60%),
50
radial-gradient(ellipse 45% 40% at 95% 100%, rgba(136,168,192,.10) 0%, transparent 60%),
51
radial-gradient(ellipse 35% 30% at 60% 40%, rgba(201,145,138,.06) 0%, transparent 60%);
52
}
53
54
/* ── HEADER ── */
55
.site-header {
56
position: sticky; top: 0; z-index: 100;
57
background: rgba(247,244,240,.88);
58
backdrop-filter: blur(20px);
59
-webkit-backdrop-filter: blur(20px);
60
border-bottom: 1px solid var(--border);
61
height: 62px;
62
display: flex; align-items: center; justify-content: space-between;
63
padding: 0 40px;
64
}
65
66
.brand { display: flex; align-items: center; gap: 11px; text-decoration: none; }
67
.brand-mark {
68
width: 36px; height: 36px; border-radius: 12px;
69
background: linear-gradient(135deg, var(--sage-mid), var(--sky-mid));
70
display: flex; align-items: center; justify-content: center;
71
font-size: 16px;
72
}
73
.brand-name {
74
font-family: 'Fraunces', serif;
75
font-size: 22px; font-weight: 600; letter-spacing: -.3px;
76
color: var(--text);
77
}
78
.brand-name span { color: var(--sage); }
79
80
.hdr-date {
81
font-family: 'DM Mono', monospace;
82
font-size: 11px; color: var(--text-3);
83
background: var(--surface2);
84
border: 1px solid var(--border);
85
border-radius: 20px; padding: 4px 12px;
86
}
87
88
/* ── LAYOUT ── */
89
.wrap {
90
position: relative; z-index: 1;
91
max-width: 1260px; margin: 0 auto;
92
padding: 40px 40px 72px;
93
}
94
95
/* ── STEP LABELS ── */
96
.step-lbl {
97
font-family: 'DM Mono', monospace;
98
font-size: 10px; letter-spacing: 2px; text-transform: uppercase;
99
color: var(--text-3); margin-bottom: 5px;
100
display: flex; align-items: center; gap: 7px;
101
}
102
.step-lbl::before {
103
content: ''; width: 16px; height: 1px;
104
background: var(--text-3);
105
}
106
.sec-title {
107
font-family: 'Fraunces', serif;
108
font-size: 26px; font-weight: 600; letter-spacing: -.4px;
109
color: var(--text); margin-bottom: 22px;
110
line-height: 1.2;
111
}
112
.sec-title em { font-style: italic; font-weight: 300; color: var(--text-2); }
113
114
/* ── FORM CARD ── */
115
.form-card {
116
background: var(--surface);
117
border: 1px solid var(--border);
118
border-radius: var(--radius-lg);
119
padding: 28px;
120
box-shadow: 0 1px 3px rgba(45,38,32,.04), 0 4px 16px rgba(45,38,32,.04);
121
}
122
123
/* ── INPUTS ── */
124
.f-label {
125
display: block; font-size: 11.5px; font-weight: 500;
126
color: var(--text-2); margin-bottom: 6px; letter-spacing: .2px;
127
}
128
.opt-tag {
129
font-size: 10px; color: var(--text-3); font-weight: 400; margin-left: 4px;
130
}
131
132
.f-input, .f-select, .f-textarea {
133
width: 100%;
134
background: var(--surface2);
135
border: 1.5px solid var(--border);
136
border-radius: 10px;
137
padding: 10px 14px;
138
color: var(--text);
139
font-family: 'DM Sans', sans-serif;
140
font-size: 13.5px;
141
outline: none;
142
transition: border-color .2s, box-shadow .2s, background .2s;
143
}
144
.f-input::placeholder, .f-textarea::placeholder { color: var(--text-3); }
145
.f-input:focus, .f-select:focus, .f-textarea:focus {
146
border-color: var(--border-focus);
147
background: var(--surface);
148
box-shadow: 0 0 0 3px rgba(138,170,140,.15);
149
}
150
.f-input[type="date"]::-webkit-calendar-picker-indicator {
151
opacity: .5; cursor: pointer;
152
}
153
.f-select {
154
cursor: pointer;
155
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23a8998e' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
156
background-repeat: no-repeat;
157
background-position: right 14px center;
158
padding-right: 38px;
159
}
160
.f-select option { background: var(--surface); color: var(--text); }
161
.f-textarea { resize: vertical; min-height: 76px; line-height: 1.65; }
162
163
/* ── PILL TOGGLE ── */
164
.pill-grp {
165
display: flex; gap: 6px;
166
}
167
.pill-opt input { display: none; }
168
.pill-opt label {
169
display: block; padding: 8px 18px;
170
background: var(--surface); border: 1.5px solid var(--border);
171
border-radius: 40px; font-size: 12.5px; font-weight: 500;
172
cursor: pointer; color: var(--text-3);
173
transition: all .18s; white-space: nowrap;
174
user-select: none;
175
}
176
.pill-opt label:hover { border-color: var(--border-focus); color: var(--text-2); background: var(--surface2); }
177
.pill-opt.sage input:checked + label {
178
background: var(--sage); border-color: var(--sage);
179
color: #fff; font-weight: 600;
180
box-shadow: 0 2px 10px rgba(138,170,140,.35);
181
}
182
.pill-opt.blush input:checked + label {
183
background: var(--blush); border-color: var(--blush);
184
color: #fff; font-weight: 600;
185
box-shadow: 0 2px 10px rgba(201,145,138,.35);
186
}
187
.pill-opt.sky input:checked + label {
188
background: var(--sky); border-color: var(--sky);
189
color: #fff; font-weight: 600;
190
box-shadow: 0 2px 10px rgba(136,168,192,.35);
191
}
192
.pill-opt.sand input:checked + label {
193
background: var(--sand); border-color: var(--sand);
194
color: #fff; font-weight: 600;
195
box-shadow: 0 2px 10px rgba(196,168,130,.35);
196
}
197
.pill-opt.lav input:checked + label {
198
background: var(--lavender); border-color: var(--lavender);
199
color: #fff; font-weight: 600;
200
box-shadow: 0 2px 10px rgba(158,146,192,.35);
201
}
202
.pill-opt input:checked + label {
203
background: var(--sage); border-color: var(--sage);
204
color: #fff; font-weight: 600;
205
}
206
207
/* ── ADD BTN ── */
208
.add-btn {
209
width: 100%; padding: 11px 16px;
210
background: transparent;
211
border: 1.5px dashed var(--border);
212
border-radius: 10px;
213
color: var(--text-3); font-family: 'DM Sans', sans-serif;
214
font-size: 13px; font-weight: 500;
215
cursor: pointer;
216
display: flex; align-items: center; justify-content: center; gap: 8px;
217
transition: all .2s;
218
}
219
.add-btn:hover {
220
border-color: var(--sage); color: var(--sage);
221
background: var(--sage-light);
222
}
223
224
/* ── DIVIDER ── */
225
.soft-divider {
226
height: 1px;
227
background: linear-gradient(90deg, transparent, var(--border), transparent);
228
margin: 22px 0;
229
}
230
231
/* ── TASK LIST ── */
232
.task-list { display: flex; flex-direction: column; gap: 7px; }
233
234
.task-card {
235
background: var(--surface);
236
border: 1px solid var(--border);
237
border-radius: 12px;
238
padding: 13px 16px;
239
display: flex; align-items: flex-start; gap: 11px;
240
animation: slideDown .22s ease;
241
transition: border-color .2s, box-shadow .2s;
242
}
243
.task-card:hover {
244
border-color: var(--border-focus);
245
box-shadow: 0 2px 12px rgba(45,38,32,.06);
246
}
247
@keyframes slideDown { from { opacity:0; transform:translateY(-8px); } to { opacity:1; transform:translateY(0); } }
248
249
.task-idx {
250
font-family: 'DM Mono', monospace;
251
font-size: 11px; color: var(--text-3);
252
min-width: 18px; padding-top: 2px;
253
}
254
.task-body { flex: 1; min-width: 0; }
255
.task-name-text {
256
font-size: 13.5px; font-weight: 500;
257
margin-bottom: 7px;
258
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
259
}
260
.task-chips { display: flex; flex-wrap: wrap; gap: 5px; }
261
262
.chip {
263
display: inline-flex; align-items: center; gap: 4px;
264
font-size: 11px; padding: 3px 9px; border-radius: 20px;
265
border: 1px solid; font-weight: 500; font-family: 'DM Sans', sans-serif;
266
}
267
.chip-deadline { color: var(--sand); background: var(--sand-light); border-color: var(--sand-mid); }
268
.chip-overdue { color: var(--blush); background: var(--blush-light); border-color: var(--blush-mid); }
269
.chip-type { color: var(--sky); background: var(--sky-light); border-color: var(--sky-mid); }
270
.chip-solo { color: var(--sage); background: var(--sage-light); border-color: var(--sage-mid); }
271
.chip-group { color: var(--lavender); background: var(--lavender-light); border-color: var(--lavender-mid); }
272
.chip-easy { color: var(--sage); background: var(--sage-light); border-color: var(--sage-mid); }
273
.chip-medium { color: var(--sand); background: var(--sand-light); border-color: var(--sand-mid); }
274
.chip-hard { color: var(--blush); background: var(--blush-light); border-color: var(--blush-mid); }
275
.chip-subject { color: var(--text-3); background: var(--surface2); border-color: var(--border); }
276
277
.task-del {
278
background: none; border: none; cursor: pointer;
279
color: var(--text-3); font-size: 13px; padding: 2px; flex-shrink: 0;
280
transition: color .15s;
281
}
282
.task-del:hover { color: var(--blush); }
283
284
/* EMPTY */
285
.empty-st {
286
text-align: center; padding: 30px 20px;
287
color: var(--text-3); font-size: 13px; line-height: 2.1;
288
}
289
.empty-icon { font-size: 28px; margin-bottom: 6px; opacity: .5; }
290
291
/* ── ANALYZE BUTTON ── */
292
.analyze-btn {
293
width: 100%; padding: 14px 20px;
294
background: var(--text);
295
border: none; border-radius: 14px;
296
color: var(--surface);
297
font-family: 'Fraunces', serif;
298
font-size: 16px; font-weight: 400; letter-spacing: .1px;
299
cursor: pointer;
300
display: flex; align-items: center; justify-content: center; gap: 10px;
301
transition: all .22s;
302
box-shadow: 0 2px 12px rgba(45,38,32,.12);
303
}
304
.analyze-btn:hover:not(:disabled) {
305
background: #3d342c;
306
transform: translateY(-1px);
307
box-shadow: 0 4px 20px rgba(45,38,32,.18);
308
}
309
.analyze-btn:disabled {
310
background: var(--surface2); color: var(--text-3);
311
box-shadow: none; cursor: not-allowed;
312
}
313
314
/* ── RESULT PANEL ── */
315
.result-sticky { position: sticky; top: 82px; }
316
317
.result-card {
318
background: var(--surface);
319
border: 1px solid var(--border);
320
border-radius: var(--radius-lg);
321
min-height: 440px; overflow: hidden;
322
box-shadow: 0 1px 3px rgba(45,38,32,.04), 0 4px 16px rgba(45,38,32,.04);
323
}
324
325
/* Placeholder */
326
.r-placeholder {
327
display: flex; flex-direction: column;
328
align-items: center; justify-content: center;
329
padding: 64px 40px; text-align: center; gap: 14px;
330
min-height: 440px;
331
}
332
.r-glyph {
333
width: 68px; height: 68px; border-radius: 50%;
334
background: var(--sage-light);
335
border: 1.5px solid var(--sage-mid);
336
display: flex; align-items: center; justify-content: center;
337
font-size: 26px;
338
}
339
.r-title { font-family: 'Fraunces', serif; font-size: 19px; font-weight: 600; color: var(--text); }
340
.r-sub { font-size: 13px; color: var(--text-3); line-height: 1.75; max-width: 240px; }
341
342
/* Loading */
343
.loading-wrap {
344
display: none; flex-direction: column;
345
align-items: center; justify-content: center;
346
gap: 16px; padding: 64px 40px; text-align: center;
347
min-height: 440px;
348
}
349
.loading-wrap.show { display: flex; }
350
.spinner {
351
width: 40px; height: 40px;
352
border: 2px solid var(--border);
353
border-top-color: var(--sage);
354
border-right-color: var(--sky);
355
border-radius: 50%;
356
animation: spin .9s linear infinite;
357
}
358
@keyframes spin { to { transform: rotate(360deg); } }
359
.loading-title { font-family: 'Fraunces', serif; font-size: 17px; font-weight: 600; color: var(--text); }
360
.loading-sub { font-size: 11.5px; color: var(--text-3); font-family: 'DM Mono', monospace; letter-spacing: .5px; }
361
.dots span {
362
display: inline-block; width: 6px; height: 6px; border-radius: 50%;
363
background: var(--sage-mid); margin: 0 2px;
364
animation: blink 1.4s ease-in-out infinite both;
365
}
366
.dots span:nth-child(2) { animation-delay: .2s; }
367
.dots span:nth-child(3) { animation-delay: .4s; }
368
@keyframes blink { 0%,80%,100%{opacity:.2;transform:scale(.8)} 40%{opacity:1;transform:scale(1)} }
369
370
/* Result body */
371
.result-body { display: none; }
372
.result-body.show { display: block; }
373
374
.r-topbar {
375
padding: 18px 24px; border-bottom: 1px solid var(--border);
376
display: flex; align-items: center; justify-content: space-between;
377
}
378
.r-topbar-title { font-family: 'Fraunces', serif; font-size: 17px; font-weight: 600; }
379
.r-badge {
380
font-family: 'DM Mono', monospace; font-size: 10px; color: var(--text-3);
381
background: var(--surface2); border: 1px solid var(--border);
382
border-radius: 20px; padding: 3px 10px;
383
}
384
385
/* Strategy box */
386
.strategy-box {
387
margin: 18px 22px;
388
padding: 16px 18px;
389
background: var(--sage-light);
390
border: 1px solid var(--sage-mid);
391
border-radius: 14px;
392
border-left: 3px solid var(--sage);
393
}
394
.strategy-lbl {
395
font-size: 10px; font-weight: 600; letter-spacing: 1.5px;
396
text-transform: uppercase; color: var(--sage); margin-bottom: 6px;
397
font-family: 'DM Mono', monospace;
398
}
399
.strategy-text { font-size: 13px; line-height: 1.75; color: var(--text-2); }
400
401
/* Priority items */
402
.p-item {
403
padding: 20px 22px; border-bottom: 1px solid var(--border);
404
animation: fadeRise .38s ease both;
405
}
406
.p-item:last-child { border-bottom: none; }
407
@keyframes fadeRise { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } }
408
409
.p-top { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 12px; }
410
411
.rank-badge {
412
min-width: 42px; height: 42px; border-radius: 14px;
413
display: flex; align-items: center; justify-content: center;
414
font-family: 'Fraunces', serif;
415
font-size: 22px; font-weight: 600; flex-shrink: 0;
416
}
417
.rk-1 { background: var(--blush-light); color: var(--blush); border: 1.5px solid var(--blush-mid); }
418
.rk-2 { background: var(--sand-light); color: var(--sand); border: 1.5px solid var(--sand-mid); }
419
.rk-3 { background: var(--sky-light); color: var(--sky); border: 1.5px solid var(--sky-mid); }
420
.rk-n { background: var(--surface2); color: var(--text-3); border: 1.5px solid var(--border); }
421
422
.p-name { font-size: 14.5px; font-weight: 600; margin-bottom: 7px; color: var(--text); }
423
.p-chips { display: flex; flex-wrap: wrap; gap: 5px; }
424
425
/* Urgency bar */
426
.urg-row { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
427
.urg-lbl {
428
font-size: 11px; color: var(--text-3); min-width: 54px;
429
font-family: 'DM Mono', monospace;
430
}
431
.urg-track {
432
flex: 1; height: 5px;
433
background: var(--surface2); border-radius: 10px; overflow: hidden;
434
}
435
.urg-fill { height: 100%; border-radius: 10px; transition: width 1.2s cubic-bezier(.16,1,.3,1); }
436
.urg-fill.critical { background: var(--blush-mid); }
437
.urg-fill.high { background: var(--sand-mid); }
438
.urg-fill.medium { background: var(--sky-mid); }
439
.urg-fill.low { background: var(--sage-mid); }
440
.urg-score {
441
font-family: 'DM Mono', monospace; font-size: 11px;
442
color: var(--text-3); min-width: 26px; text-align: right;
443
}
444
445
/* Reason box */
446
.reason-box {
447
background: var(--surface2); border: 1px solid var(--border);
448
border-left: 2.5px solid var(--lavender-mid);
449
border-radius: 0 10px 10px 0;
450
padding: 12px 14px;
451
}
452
.reason-head {
453
font-size: 10px; font-weight: 600; letter-spacing: 1.5px;
454
text-transform: uppercase; color: var(--lavender); margin-bottom: 5px;
455
font-family: 'DM Mono', monospace;
456
}
457
.reason-text { font-size: 12.5px; line-height: 1.75; color: var(--text-2); }
458
.tip-row {
459
margin-top: 9px; padding-top: 9px;
460
border-top: 1px solid var(--border);
461
font-size: 12px; color: var(--sand);
462
display: flex; gap: 7px; align-items: flex-start;
463
}
464
465
/* SCROLLBAR */
466
::-webkit-scrollbar { width: 4px; }
467
::-webkit-scrollbar-track { background: transparent; }
468
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
469
470
@media (max-width: 920px) {
471
.main-grid { flex-direction: column; }
472
.result-sticky { position: static; }
473
.site-header, .wrap { padding-left: 18px; padding-right: 18px; }
474
.wrap { padding-top: 24px; }
475
}
HTML challenge15-webdev-html 172 lines (149 loc) | 8.13 KB
1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
<meta charset="UTF-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6
<title>Taskly — AI Priority Planner</title>
7
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400&family=Fraunces:ital,wght@0,300;0,400;0,600;1,300;1,400&family=DM+Mono:wght@300;400&display=swap" rel="stylesheet">
8
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
9
<link rel="stylesheet" href="style.css">
10
</head>
11
<body>
12
<!-- HEADER -->
13
<header class="site-header">
14
<a class="brand" href="#">
15
<div class="brand-mark"></div>
16
<span class="brand-name">Task<span>ly</span></span>
17
</a>
18
<span class="hdr-date" id="headerDate"></span>
19
</header>
20
21
<div id="corsBanner" style="display:none;background:#fef3dc;border-bottom:1px solid #dfc9a8;padding:10px 40px;font-size:12.5px;color:#7a5c2a;font-family:'DM Sans',sans-serif;text-align:center">
22
<strong>Open this file via a local server</strong> for the AI to work.
23
In your terminal: <code style="background:rgba(0,0,0,.07);padding:2px 7px;border-radius:4px">npx serve .</code> — then open the printed URL in your browser.
24
</div>
25
<script>
26
if (location.protocol === 'file:') document.getElementById('corsBanner').style.display = 'block';
27
</script>
28
29
30
<div class="wrap">
31
<div class="row g-4" style="--bs-gutter-x:32px">
32
33
<!-- LEFT -->
34
<div class="col-12 col-lg-5">
35
<div class="step-lbl">Step 01</div>
36
<div class="sec-title">Add this week's <em>tasks</em></div>
37
38
<div class="form-card mb-3">
39
<div class="mb-3">
40
<label class="f-label">Task Name <span style="color:var(--blush)">*</span></label>
41
<input class="f-input" id="taskName" placeholder="e.g. Organic Chemistry Lab Report" autocomplete="off"/>
42
</div>
43
44
<div class="row g-3 mb-3">
45
<div class="col-7">
46
<label class="f-label">Deadline <span style="color:var(--blush)">*</span></label>
47
<input class="f-input" id="taskDeadline" type="date"/>
48
</div>
49
<div class="col-5">
50
<label class="f-label">Est. Hours <span class="opt-tag">(optional)</span></label>
51
<input class="f-input" id="taskHours" type="number" min="0.5" max="99" step="0.5" placeholder="3"/>
52
</div>
53
</div>
54
55
<div class="row g-3 mb-3">
56
<div class="col-6">
57
<label class="f-label">Task Type</label>
58
<select class="f-select" id="taskType">
59
<option value="">— Select —</option>
60
<option>Lab Report</option>
61
<option>Essay / Paper</option>
62
<option>Presentation</option>
63
<option>Quiz / Exam</option>
64
<option>Coding / Project</option>
65
<option>Practicum</option>
66
<option>Research</option>
67
<option>Routine Assignment</option>
68
<option>Other</option>
69
</select>
70
</div>
71
<div class="col-6">
72
<label class="f-label">Subject / Course</label>
73
<input class="f-input" id="taskSubject" placeholder="e.g. Organic Chem"/>
74
</div>
75
</div>
76
77
<div class="mb-3">
78
<label class="f-label">Difficulty</label>
79
<div class="pill-grp">
80
<div class="pill-opt sage"><input type="radio" name="diff" id="d1" value="Easy"><label for="d1">Easy</label></div>
81
<div class="pill-opt sand"><input type="radio" name="diff" id="d2" value="Medium" checked><label for="d2">Medium</label></div>
82
<div class="pill-opt blush"><input type="radio" name="diff" id="d3" value="Hard"><label for="d3">Hard</label></div>
83
</div>
84
</div>
85
86
<div class="mb-3">
87
<label class="f-label">Work Type</label>
88
<div class="pill-grp">
89
<div class="pill-opt sky"><input type="radio" name="ptype" id="p1" value="Individual" checked><label for="p1">👤 Individual</label></div>
90
<div class="pill-opt lav"><input type="radio" name="ptype" id="p2" value="Group"><label for="p2">👥 Group</label></div>
91
</div>
92
</div>
93
94
<div class="mb-3">
95
<label class="f-label">Grade Weight</label>
96
<div class="pill-grp flex-wrap" style="gap:6px">
97
<div class="pill-opt sage"><input type="radio" name="weight" id="w1" value="Low"><label for="w1">Low</label></div>
98
<div class="pill-opt sand"><input type="radio" name="weight" id="w2" value="Medium" checked><label for="w2">Medium</label></div>
99
<div class="pill-opt sky"><input type="radio" name="weight" id="w3" value="High"><label for="w3">High</label></div>
100
<div class="pill-opt blush"><input type="radio" name="weight" id="w4" value="Final"><label for="w4">Final / Exam</label></div>
101
</div>
102
</div>
103
104
<div class="mb-3">
105
<label class="f-label">Notes <span class="opt-tag">(optional)</span></label>
106
<textarea class="f-textarea" id="taskNotes" placeholder="e.g. Need lab access, coordinate with teammates first..."></textarea>
107
</div>
108
109
<button class="add-btn" onclick="addTask()">
110
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="7.5" cy="7.5" r="6.5"/><line x1="7.5" y1="4.5" x2="7.5" y2="10.5"/><line x1="4.5" y1="7.5" x2="10.5" y2="7.5"/></svg> Add to List
111
</button>
112
</div>
113
114
<!-- TASK LIST -->
115
<div class="d-flex align-items-center justify-content-between mb-2 px-1">
116
<div class="step-lbl mb-0">Step 02</div>
117
<span id="taskCount" style="font-family:'DM Mono',monospace;font-size:11px;color:var(--text-3)">0 tasks</span>
118
</div>
119
120
<div class="task-list mb-3" id="taskList">
121
<div class="empty-st">
122
<div class="empty-icon">🗒️</div>
123
No tasks yet.<br>Fill out the form above and click <strong style="color:var(--text)">"Add to List"</strong>
124
</div>
125
</div>
126
127
<button class="analyze-btn" id="analyzeBtn" onclick="analyzeTask()" disabled>
128
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l1.5 3.5L13 6l-2.5 2.5.6 3.5L8 10.4l-3.1 1.6.6-3.5L3 6l3.5-.5L8 1z"/></svg> Analyze &amp; Prioritize
129
</button>
130
</div>
131
132
<!-- RIGHT -->
133
<div class="col-12 col-lg-7">
134
<div class="result-sticky">
135
<div class="step-lbl">Step 03</div>
136
<div class="sec-title" style="color:var(--sage);">AI <em>Recommendations</em></div>
137
138
<div class="result-card">
139
140
<div class="r-placeholder" id="placeholder">
141
<div class="r-glyph">🌿</div>
142
<div class="r-title">Waiting for your tasks</div>
143
<div class="r-sub">Add at least one task, then click <strong>Analyze &amp; Prioritize</strong> to get a calm, clear plan.</div>
144
</div>
145
146
<div class="loading-wrap" id="loadingWrap">
147
<div class="spinner"></div>
148
<div class="loading-title">Thinking it through…</div>
149
<div class="loading-sub">deadlines · difficulty · workload</div>
150
<div class="dots"><span></span><span></span><span></span></div>
151
</div>
152
153
<div class="result-body" id="resultBody">
154
<div class="r-topbar">
155
<div class="r-topbar-title">Priority Order</div>
156
<div class="r-badge" id="resultMeta"></div>
157
</div>
158
<div id="strategyWrap"></div>
159
<div id="priorityList"></div>
160
</div>
161
162
</div>
163
</div>
164
</div>
165
166
</div>
167
</div>
168
169
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
170
<script src="script.js"></script>
171
</body>
172
</html>
JavaScript challenge15-webdev-js 507 lines (420 loc) | 18.43 KB
1
// Initialize page
2
let tasks = [];
3
const today = new Date().toISOString().split('T')[0];
4
5
document.getElementById('headerDate').textContent = new Date().toLocaleDateString('en-US', {weekday:'short', month:'short', day:'numeric', year:'numeric'});
6
document.getElementById('taskDeadline').min = today;
7
8
// Get radio button value
9
function getRadioValue(radioName) {
10
const element = document.querySelector('input[name="' + radioName + '"]:checked');
11
if (element) {
12
return element.value;
13
}
14
return '';
15
}
16
17
// Show error highlight on input
18
function showError(elementId) {
19
const element = document.getElementById(elementId);
20
element.style.borderColor = 'var(--blush)';
21
element.style.boxShadow = '0 0 0 3px rgba(201,145,138,.18)';
22
23
setTimeout(function() {
24
element.style.borderColor = '';
25
element.style.boxShadow = '';
26
}, 1500);
27
}
28
29
// Add new task
30
function addTask() {
31
const taskName = document.getElementById('taskName').value.trim();
32
const taskDeadline = document.getElementById('taskDeadline').value;
33
34
// Check if required fields have value
35
if (taskName === '') {
36
showError('taskName');
37
}
38
if (taskDeadline === '') {
39
showError('taskDeadline');
40
}
41
42
// Stop if fields are empty
43
if (taskName === '' || taskDeadline === '') {
44
return;
45
}
46
47
// Get form values
48
let taskHours = document.getElementById('taskHours').value;
49
if (taskHours === '') {
50
taskHours = '?';
51
}
52
53
let taskType = document.getElementById('taskType').value;
54
if (taskType === '') {
55
taskType = 'Other';
56
}
57
58
const taskSubject = document.getElementById('taskSubject').value.trim();
59
const taskDifficulty = getRadioValue('diff');
60
const taskWorkType = getRadioValue('ptype');
61
const taskWeight = getRadioValue('weight');
62
const taskNotes = document.getElementById('taskNotes').value.trim();
63
64
// Create task object
65
const newTask = {
66
id: Date.now(),
67
name: taskName,
68
deadline: taskDeadline,
69
hours: taskHours,
70
type: taskType,
71
subject: taskSubject,
72
difficulty: taskDifficulty,
73
workType: taskWorkType,
74
weight: taskWeight,
75
notes: taskNotes
76
};
77
78
// Add task to list
79
tasks.push(newTask);
80
81
// Update display
82
renderList();
83
resetForm();
84
}
85
86
// Clear form to empty state
87
function resetForm() {
88
// Clear text inputs
89
document.getElementById('taskName').value = '';
90
document.getElementById('taskHours').value = '';
91
document.getElementById('taskSubject').value = '';
92
document.getElementById('taskNotes').value = '';
93
document.getElementById('taskDeadline').value = '';
94
document.getElementById('taskType').value = '';
95
96
// Reset radio buttons to defaults
97
document.querySelector('input[name="diff"][value="Medium"]').checked = true;
98
document.querySelector('input[name="ptype"][value="Individual"]').checked = true;
99
document.querySelector('input[name="weight"][value="Medium"]').checked = true;
100
}
101
102
// Remove task by ID
103
function removeTask(taskId) {
104
// Create new array without the deleted task
105
const newTasks = [];
106
for (let i = 0; i < tasks.length; i++) {
107
if (tasks[i].id !== taskId) {
108
newTasks.push(tasks[i]);
109
}
110
}
111
tasks = newTasks;
112
renderList();
113
}
114
115
// Calculate days until deadline
116
function daysLeft(deadline) {
117
const today = new Date();
118
const dueDate = new Date(deadline);
119
const difference = dueDate - today;
120
const millisecondsInDay = 86400000;
121
const days = Math.ceil(difference / millisecondsInDay);
122
return days;
123
}
124
125
// Show task list
126
function renderList() {
127
const taskCount = tasks.length;
128
129
// Update task count text
130
let countText = taskCount + ' task';
131
if (taskCount !== 1) {
132
countText = taskCount + ' tasks';
133
}
134
document.getElementById('taskCount').textContent = countText;
135
136
// Enable/disable analyze button
137
if (taskCount === 0) {
138
document.getElementById('analyzeBtn').disabled = true;
139
} else {
140
document.getElementById('analyzeBtn').disabled = false;
141
}
142
143
// Show empty state if no tasks
144
if (taskCount === 0) {
145
const emptyMessage = '<div class="empty-st"><div class="empty-icon">🗒️</div>No tasks yet.<br>Fill out the form above and click <strong style="color:var(--text)">"Add to List"</strong></div>';
146
document.getElementById('taskList').innerHTML = emptyMessage;
147
return;
148
}
149
150
// Build HTML for each task
151
let listHTML = '';
152
for (let i = 0; i < tasks.length; i++) {
153
const task = tasks[i];
154
const taskNumber = i + 1;
155
const daysRemaining = daysLeft(task.deadline);
156
157
// Determine deadline style
158
let deadlineClass = 'deadline';
159
let deadlineText = '';
160
if (daysRemaining < 0) {
161
deadlineClass = 'overdue';
162
deadlineText = 'Overdue ' + Math.abs(daysRemaining) + 'd';
163
} else if (daysRemaining === 0) {
164
deadlineClass = 'deadline';
165
deadlineText = 'Due today!';
166
} else {
167
deadlineClass = 'deadline';
168
deadlineText = daysRemaining + 'd left';
169
}
170
171
// Determine difficulty style
172
let difficultyClass = 'medium';
173
if (task.difficulty === 'Easy') {
174
difficultyClass = 'easy';
175
} else if (task.difficulty === 'Hard') {
176
difficultyClass = 'hard';
177
}
178
179
// Determine work type icon and style
180
let workTypeClass = 'solo';
181
let workTypeIcon = '👤';
182
if (task.workType === 'Group') {
183
workTypeClass = 'group';
184
workTypeIcon = '👥';
185
}
186
187
// Build subject chip if exists
188
let subjectChip = '';
189
if (task.subject !== '') {
190
subjectChip = '<span class="chip chip-subject">' + task.subject + '</span>';
191
}
192
193
// Build task card HTML
194
const taskCard = '<div class="task-card">' +
195
'<span class="task-idx">' + taskNumber + '.</span>' +
196
'<div class="task-body">' +
197
'<div class="task-name-text" title="' + task.name + '">' + task.name + '</div>' +
198
'<div class="task-chips">' +
199
'<span class="chip chip-' + deadlineClass + '"><svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" style="flex-shrink:0"><circle cx="5" cy="5" r="4"/><polyline points="5,2.5 5,5 6.5,6.5"/></svg> ' + deadlineText + '</span>' +
200
'<span class="chip chip-type">' + task.type + '</span>' +
201
'<span class="chip chip-' + workTypeClass + '">' + workTypeIcon + ' ' + task.workType + '</span>' +
202
'<span class="chip chip-' + difficultyClass + '">' + task.difficulty + '</span>' +
203
subjectChip +
204
'</div>' +
205
'</div>' +
206
'<button class="task-del" onclick="removeTask(' + task.id + ')"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="2" y1="2" x2="10" y2="10"/><line x1="10" y1="2" x2="2" y2="10"/></svg></button>' +
207
'</div>';
208
209
listHTML = listHTML + taskCard;
210
}
211
212
document.getElementById('taskList').innerHTML = listHTML;
213
}
214
215
// Analyze tasks with AI
216
async function analyzeTask() {
217
// Check if there are no tasks in the tasks array.
218
// If there are zero tasks, stop the function using return.
219
220
// Hide the placeholder element so the result area becomes empty.
221
// Target element id: "placeholder"
222
// Change its display style to "none".
223
224
// Show the loading animation.
225
// Target element id: "loadingWrap"
226
// Add the CSS class "show".
227
228
// Disable the analyze button so the user cannot click it again during loading.
229
// Target element id: "analyzeBtn"
230
231
try {
232
233
// Create an empty string variable that will store the formatted task information.
234
let taskData = '';
235
236
// Loop through every task inside the tasks array.
237
// Use a for loop with index i.
238
for (let i = 0; i < tasks.length; i++) {
239
240
const task = tasks[i];
241
242
// Create a task number starting from 1 instead of 0.
243
const index =
244
245
// Calculate the remaining days before the deadline.
246
// Use the helper function daysLeft() and pass task.deadline.
247
const daysRemaining =
248
249
// This variable will store the readable deadline text.
250
let deadlineStatus = '';
251
252
// Create a condition:
253
// If daysRemaining is less than 0 → the task is overdue.
254
// Show: "OVERDUE by X days".
255
// Otherwise show: "X days from today".
256
257
// If the task subject is empty (""), replace it with "not specified".
258
// Otherwise keep the original subject.
259
const subject =
260
261
// If the task notes are empty (""), replace them with "none".
262
// Otherwise keep the original notes.
263
const notes =
264
265
// Build the taskData string by adding information for each task.
266
// Include the following information exactly like this format:
267
//
268
// Task 1:
269
// - Name: ...
270
// - Subject: ...
271
// - Type: ...
272
// - Deadline: ... (deadlineStatus)
273
// - Estimated time: ... hours
274
// - Difficulty: ...
275
// - Work type: ...
276
// - Grade weight: ...
277
// - Notes: ...
278
//
279
// Use string concatenation to append this information to taskData.
280
281
}
282
283
// Create the AI prompt.
284
// The prompt should tell the AI that it is a study planner.
285
// It must include:
286
// - today's date (use the variable "today")
287
// - the formatted taskData string
288
// - instructions asking the AI to rank tasks by priority
289
//
290
// Store the final prompt inside a variable called prompt.
291
const prompt =
292
293
// Send the request to the Gemini API using fetch().
294
const response = await fetch('PASTE_API_URL_HERE', {
295
296
// Use the POST method.
297
method:
298
299
// Add headers with Content-Type set to application/json.
300
headers:
301
302
// Convert the request body object into JSON using JSON.stringify().
303
// The body should contain:
304
// - contents with the prompt text
305
// - generationConfig with temperature and maxOutputTokens
306
body:
307
308
});
309
310
// Check if the API request failed.
311
// If response.ok is false, throw a new Error with message "API error".
312
313
// Convert the response into JSON format.
314
const data =
315
316
// Create a variable to store the AI response text.
317
let rawResponse = '';
318
319
// Extract the AI text from the following location:
320
// data.candidates[0].content.parts[0].text
321
// Store the text inside rawResponse.
322
323
// If rawResponse is empty, throw a new Error with message "Empty response".
324
325
// Clean the JSON text by removing markdown formatting such as ```json
326
let cleanedJSON = rawResponse
327
.replace(/```json/g, '')
328
.replace(/```/g, '')
329
.trim();
330
331
// Try to extract the JSON object if the AI added extra text.
332
// Use a regular expression to match the content between { and }.
333
let jsonMatch = cleanedJSON.match(/\{[\s\S]*\}/);
334
335
if (jsonMatch) {
336
cleanedJSON = jsonMatch[0];
337
}
338
339
// Convert the cleaned JSON string into a JavaScript object.
340
// Use JSON.parse().
341
let result =
342
343
// Display the result by calling the renderResult() function
344
// and passing the parsed result as the argument.
345
346
} catch (error) {
347
348
// If an error happens:
349
// 1. Hide the loading animation (remove "show" class from loadingWrap)
350
// 2. Show the placeholder again (display = "flex")
351
352
const errorMessage = error.message === '' ? 'Unknown error' : error.message;
353
354
const errorHTML =
355
'<div class="r-glyph">🌧️</div>' +
356
'<div class="r-title">Something went wrong</div>' +
357
'<div class="r-sub">' + errorMessage + '</div>';
358
359
// Insert the errorHTML into the placeholder element.
360
361
console.error(error);
362
}
363
364
// Re-enable the Analyze button by setting disabled to false.
365
366
}
367
368
// Convert urgency level to CSS class
369
function getUrgencyClass(urgencyLevel) {
370
if (urgencyLevel === 'Critical') {
371
return 'critical';
372
} else if (urgencyLevel === 'High') {
373
return 'high';
374
} else if (urgencyLevel === 'Medium') {
375
return 'medium';
376
} else {
377
return 'low';
378
}
379
}
380
381
// Get rank badge class
382
function getRankClass(index) {
383
if (index === 0) {
384
return 'rk-1';
385
} else if (index === 1) {
386
return 'rk-2';
387
} else if (index === 2) {
388
return 'rk-3';
389
} else {
390
return 'rk-n';
391
}
392
}
393
394
// Find task by name
395
function findTaskByName(taskName) {
396
for (let i = 0; i < tasks.length; i++) {
397
if (tasks[i].name === taskName) {
398
return tasks[i];
399
}
400
}
401
return null;
402
}
403
404
// Display results
405
function renderResult(data) {
406
document.getElementById('loadingWrap').classList.remove('show');
407
408
// Show timestamp
409
const now = new Date();
410
const timeString = now.toLocaleTimeString('en-US', {hour: '2-digit', minute: '2-digit'});
411
const taskWord = tasks.length === 1 ? 'task' : 'tasks';
412
document.getElementById('resultMeta').textContent = tasks.length + ' ' + taskWord + ' · ' + timeString;
413
414
// Show strategy
415
const strategyHTML = '<div class="strategy-box"><div class="strategy-lbl">✦ Weekly Strategy</div><div class="strategy-text">' + data.summary + '</div></div>';
416
document.getElementById('strategyWrap').innerHTML = strategyHTML;
417
418
// Build priority list
419
let prioritiesHTML = '';
420
for (let i = 0; i < data.priorities.length; i++) {
421
const priority = data.priorities[i];
422
const originalTask = findTaskByName(priority.taskName);
423
424
const rankClass = getRankClass(i);
425
const urgencyClass = getUrgencyClass(priority.urgencyLevel);
426
const score = priority.urgencyScore || 50;
427
428
// Get deadline info from original task
429
let deadlineChip = '';
430
if (originalTask !== null) {
431
const daysRemaining = daysLeft(originalTask.deadline);
432
let deadlineClass = 'deadline';
433
let deadlineText = '';
434
435
if (daysRemaining < 0) {
436
deadlineClass = 'overdue';
437
deadlineText = 'Overdue ' + Math.abs(daysRemaining) + 'd';
438
} else if (daysRemaining === 0) {
439
deadlineClass = 'deadline';
440
deadlineText = 'Due today!';
441
} else {
442
deadlineClass = 'deadline';
443
deadlineText = daysRemaining + 'd left';
444
}
445
446
deadlineChip = '<span class="chip chip-' + deadlineClass + '"><svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" style="flex-shrink:0"><circle cx="5" cy="5" r="4"/><polyline points="5,2.5 5,5 6.5,6.5"/></svg> ' + deadlineText + '</span>';
447
}
448
449
// Get type chip
450
let typeChip = '';
451
if (originalTask !== null && originalTask.type !== '') {
452
typeChip = '<span class="chip chip-type">' + originalTask.type + '</span>';
453
}
454
455
// Get subject chip
456
let subjectChip = '';
457
if (originalTask !== null && originalTask.subject !== '') {
458
subjectChip = '<span class="chip chip-subject">' + originalTask.subject + '</span>';
459
}
460
461
// Get tip section
462
let tipHTML = '';
463
if (priority.tip !== '' && priority.tip !== undefined) {
464
tipHTML = '<div class="tip-row"><svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" style="flex-shrink:0;margin-top:2px"><path d="M6.5 1.5a4 4 0 0 1 2 7.4V10a .5.5 0 0 1-.5.5h-3A.5.5 0 0 1 4.5 10V8.9a4 4 0 0 1 2-7.4z"/><line x1="4.5" y1="11.5" x2="8.5" y2="11.5"/></svg><span>' + priority.tip + '</span></div>';
465
}
466
467
// Build priority item HTML
468
const priorityDelay = i * 0.07;
469
const priorityHTML = '<div class="p-item" style="animation-delay:' + priorityDelay + 's">' +
470
'<div class="p-top">' +
471
'<div class="rank-badge ' + rankClass + '">' + priority.rank + '</div>' +
472
'<div style="flex:1;min-width:0">' +
473
'<div class="p-name">' + priority.taskName + '</div>' +
474
'<div class="p-chips">' +
475
deadlineChip +
476
typeChip +
477
subjectChip +
478
'</div>' +
479
'</div>' +
480
'</div>' +
481
'<div class="urg-row">' +
482
'<span class="urg-lbl">' + priority.urgencyLevel + '</span>' +
483
'<div class="urg-track"><div class="urg-fill ' + urgencyClass + '" style="width:0%" data-w="' + score + '%"></div></div>' +
484
'<span class="urg-score">' + score + '</span>' +
485
'</div>' +
486
'<div class="reason-box">' +
487
'<div class="reason-head">AI Reasoning</div>' +
488
'<div class="reason-text">' + priority.reason + '</div>' +
489
tipHTML +
490
'</div>' +
491
'</div>';
492
493
prioritiesHTML = prioritiesHTML + priorityHTML;
494
}
495
496
document.getElementById('priorityList').innerHTML = prioritiesHTML;
497
document.getElementById('resultBody').classList.add('show');
498
499
// Animate progress bars
500
setTimeout(function() {
501
const bars = document.querySelectorAll('.urg-fill[data-w]');
502
for (let i = 0; i < bars.length; i++) {
503
const width = bars[i].getAttribute('data-w');
504
bars[i].style.width = width;
505
}
506
}, 10);
507
}